Apache Shiro 簡介
使用 Apache Shiro 爲 web 應用程序進行用戶身份驗證
Shiro 是一個 Apache Incubator 項目,旨在簡化身份驗證和授權。在本文中,瞭解 Apache Shiro 並通過示例來在一個 Groovy web 應用程序中嘗試使用 Shiro 進行身份驗證和授權。
Apache Shiro 是一個框架,可用於身份驗證和授權。本文提供了幾個示例用來展示如何在 Java™ 應用程序中使用 Shiro 並給出瞭如何在一個 Grails web 應用程序中使用它的概述。爲了從本文中最大限度地受益,您應該習慣於創建 Java 應用程序並安裝瞭如下的幾個組件:
- Java 1.6 JDK
- Grails(用來運行這些 web 應用程序示例)
常用縮略詞
- API: 應用程序編程接口
- HTTP: 超文本傳輸協議
- JAR: Java 檔案文件
- JDBC: Java 數據庫連接
- JDK: Java 開發工具包
- LDAP:輕量級目錄訪問協議
身份驗證和授權
在對系統進行安全保障時,有兩個安全性元素非常重要:身份驗證和授權。雖然這兩個術語代表的是不同的含義,但出於它們在應用程序安全性方面各自的角色考慮,它們有時會被交換使用。
身份驗證 指的是驗證用戶的身份。在驗證用戶身份時,需要確認用戶的身份的確如他們所聲稱的那樣。在大多數應用程序中,身份驗證是通過用戶名和密碼的組合完成的。只要用戶選擇了他人很難猜到的密碼,那麼用戶名和密碼的組合通常就足以確立身份。但是,還有其他的身份驗證方式可用,比如指紋、證書和生成鍵。
一旦身份驗證過程成功地建立起身份,授權 就會接管以便進行訪問的限制或允許。 所以,有這樣的可能性:用戶雖然通過了身份驗證可以登錄到一個系統,但是未經過授權,不準做任何事情。還有一種可能是用戶雖然具有了某種程度的授權,卻並未經過身份驗證。
在爲應用程序規劃安全性模型時,必須處理好這兩個元素以確保系統具有足夠的安全性。身份驗證是應用程序常見的問題(特別是在只有用戶和密碼組合的情況下),所以讓框架來處理這項工作是一個很好的做法。合理的框架可提供經過測試和維護的優勢,讓您可以集中精力處理業務問題,而不是解決其解決方案已經實現的問題。
Apache Shiro 提供了一個可用的安全性框架,各種客戶機都可將這個框架應用於它們的應用程序。本文中的這些例子旨在介紹 Shiro 並着重展示對用戶進行身份驗證的基本任務。
瞭解 Shiro
Shiro 是一個用 Java 語言實現的框架,通過一個簡單易用的 API 提供身份驗證和授權。使用 Shiro,您就能夠爲您的應用程序提供安全性而又無需從頭編寫所有代碼。
Shiro 的 Session
對象允許無需 HttpSession
即可使用一個用戶會話。通過使用一個通用的Session
對象,即便該代碼沒有在一個
Web 應用程序中運行,仍可以使用相同的代碼。沒有對應用服務器或 Web 應用服務器會話管理的依賴,您甚至可以在命令行環境中使用 Shiro。換言之,使用 Shiro 的 API 編寫的代碼讓您可以構建連接到 LDAP 服務器的命令行應用程序並且與 web 應用程序內用來訪問 LDAP 服務器的代碼相同。由於 Shiro 提供具有諸多不同數據源的身份驗證,以及 Enterprise Session Management,所以是實現單點登錄(SSO)的理想之選 — 大型企業內的一個理想特性,因爲在大型企業內,用戶需要在一天內經常登錄到並使用不同系統。這些數據源包括
JDBC、LDAP、 Kerberos 和 Microsoft® Active Directory® Directory Services (AD DS)。
目錄貼: 跟我學Shiro目錄貼
6.1 Realm
【2.5 Realm】及【3.5 Authorizer】部分都已經詳細介紹過Realm了,接下來再來看一下一般真實環境下的Realm如何實現。
1 、定義實體及關係
即用戶-角色之間是多對多關係,角色-權限之間是多對多關係;且用戶和權限之間通過角色建立關係;在系統中驗證時通過權限驗證,角色只是權限集合,即所謂的顯示角色;其實權限應該對應到資源(如菜單、URL、頁面按鈕、Java方法等)中,即應該將權限字符串存儲到資源實體中,但是目前爲了簡單化,直接提取一個權限表,【綜合示例】部分會使用完整的表結構。
用戶實體包括:編號(id)、用戶名(username)、密碼(password)、鹽(salt)、是否鎖定(locked);是否鎖定用於封禁用戶使用,其實最好使用Enum字段存儲,可以實現更復雜的用戶狀態實現。
角色實體包括:、編號(id)、角色標識符(role)、描述(description)、是否可用(available);其中角色標識符用於在程序中進行隱式角色判斷的,描述用於以後再前臺界面顯示的、是否可用表示角色當前是否激活。
權限實體包括:編號(id)、權限標識符(permission)、描述(description)、是否可用(available);含義和角色實體類似不再闡述。
另外還有兩個關係實體:用戶-角色實體(用戶編號、角色編號,且組合爲複合主鍵);角色-權限實體(角色編號、權限編號,且組合爲複合主鍵)。
sql及實體請參考源代碼中的sql\shiro.sql 和 com.github.zhangkaitao.shiro.chapter6.entity對應的實體。
2 、環境準備
爲了方便數據庫操作,使用了“org.springframework: spring-jdbc: 4.0.0.RELEASE”依賴,雖然是spring4版本的,但使用上和spring3無區別。其他依賴請參考源碼的pom.xml。
3 、定義 Service 及 Dao
爲了實現的簡單性,只實現必須的功能,其他的可以自己實現即可。
PermissionService
public interface PermissionService {
public Permission createPermission(Permission permission);
public void deletePermission(Long permissionId);
}
實現基本的創建/刪除權限。
RoleService
public interface RoleService {
public Role createRole(Role role);
public void deleteRole(Long roleId);
//添加角色-權限之間關係
public void correlationPermissions(Long roleId, Long... permissionIds);
//移除角色-權限之間關係
public void uncorrelationPermissions(Long roleId, Long... permissionIds);//
}
相對於PermissionService多了關聯/移除關聯角色-權限功能。
UserService
public interface UserService {
public User createUser(User user); //創建賬戶
public void changePassword(Long userId, String newPassword);//修改密碼
public void correlationRoles(Long userId, Long... roleIds); //添加用戶-角色關係
public void uncorrelationRoles(Long userId, Long... roleIds);// 移除用戶-角色關係
public User findByUsername(String username);// 根據用戶名查找用戶
public Set<String> findRoles(String username);// 根據用戶名查找其角色
public Set<String> findPermissions(String username); //根據用戶名查找其權限
}
此處使用findByUsername、findRoles及findPermissions來查找用戶名對應的帳號、角色及權限信息。之後的Realm就使用這些方法來查找相關信息。
UserServiceImpl
public User createUser(User user) { //加密密碼 passwordHelper.encryptPassword(user); return userDao.createUser(user); } public void changePassword(Long userId, String newPassword) { User user =userDao.findOne(userId); user.setPassword(newPassword); passwordHelper.encryptPassword(user); userDao.updateUser(user); }
在創建賬戶及修改密碼時直接把生成密碼操作委託給PasswordHelper。
PasswordHelper
public class PasswordHelper {
private RandomNumberGenerator randomNumberGenerator =
new SecureRandomNumberGenerator();
private String algorithmName = "md5";
private final int hashIterations = 2;
public void encryptPassword(User user) {
user.setSalt(randomNumberGenerator.nextBytes().toHex());
String newPassword = new SimpleHash(
algorithmName,
user.getPassword(),
ByteSource.Util.bytes(user.getCredentialsSalt()),
hashIterations).toHex();
user.setPassword(newPassword);
}
}
之後的CredentialsMatcher需要和此處加密的算法一樣。user.getCredentialsSalt()輔助方法返回username+salt。
爲了節省篇幅,對於DAO/Service的接口及實現,具體請參考源碼com.github.zhangkaitao.shiro.chapter6。另外請參考Service層的測試用例com.github.zhangkaitao.shiro.chapter6.service.ServiceTest。
4 、定義 Realm
RetryLimitHashedCredentialsMatcher
和第五章的一樣,在此就不羅列代碼了,請參考源碼com.github.zhangkaitao.shiro.chapter6.credentials.RetryLimitHashedCredentialsMatcher。
UserRealm
另外請參考Service層的測試用例com.github.zhangkaitao.shiro.chapter6.service.ServiceTest。
public class UserRealm extends AuthorizingRealm {
private UserService userService = new UserServiceImpl();
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(userService.findRoles(username));
authorizationInfo.setStringPermissions(userService.findPermissions(username));
return authorizationInfo;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null) {
throw new UnknownAccountException();//沒找到帳號
}
if(Boolean.TRUE.equals(user.getLocked())) {
throw new LockedAccountException(); //帳號鎖定
}
//交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,如果覺得人家的不好可以在此判斷或自定義實現
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getUsername(), //用戶名
user.getPassword(), //密碼
ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
}
1 、UserRealm 父類AuthorizingRealm 將獲取Subject 相關信息分成兩步 :獲取身份驗證信息(doGetAuthenticationInfo)及授權信息(doGetAuthorizationInfo);
2 、doGetAuthenticationInfo 獲取身份驗證相關信息 :首先根據傳入的用戶名獲取User信息;然後如果user爲空,那麼拋出沒找到帳號異常UnknownAccountException;如果user找到但鎖定了拋出鎖定異常LockedAccountException;最後生成AuthenticationInfo信息,交給間接父類AuthenticatingRealm使用CredentialsMatcher進行判斷密碼是否匹配,如果不匹配將拋出密碼錯誤異常IncorrectCredentialsException;另外如果密碼重試此處太多將拋出超出重試次數異常ExcessiveAttemptsException;在組裝SimpleAuthenticationInfo信息時,需要傳入:身份信息(用戶名)、憑據(密文密碼)、鹽(username+salt),CredentialsMatcher使用鹽加密傳入的明文密碼和此處的密文密碼進行匹配。
3 、doGetAuthorizationInfo 獲取授權信息 :PrincipalCollection是一個身份集合,因爲我們現在就一個Realm,所以直接調用getPrimaryPrincipal得到之前傳入的用戶名即可;然後根據用戶名調用UserService接口獲取角色及權限信息。
5 、測試用例
爲了節省篇幅,請參考測試用例com.github.zhangkaitao.shiro.chapter6.realm.UserRealmTest。包含了:登錄成功、用戶名錯誤、密碼錯誤、密碼超出重試次數、有/沒有角色、有/沒有權限的測試。
6.2 AuthenticationToken
AuthenticationToken用於收集用戶提交的身份(如用戶名)及憑據(如密碼):
public interface AuthenticationToken extends Serializable {
Object getPrincipal(); //身份
Object getCredentials(); //憑據
}
擴展接口RememberMeAuthenticationToken:提供了“boolean isRememberMe()”現“記住我”的功能;
擴展接口是HostAuthenticationToken:提供了“String getHost()”方法用於獲取用戶“主機”的功能。
Shiro提供了一個直接拿來用的UsernamePasswordToken,用於實現用戶名/密碼Token組,另外其實現了RememberMeAuthenticationToken和HostAuthenticationToken,可以實現記住我及主機驗證的支持。
6.3 AuthenticationInfo
AuthenticationInfo有兩個作用:
1、如果Realm是AuthenticatingRealm子類,則提供給AuthenticatingRealm內部使用的CredentialsMatcher進行憑據驗證;(如果沒有繼承它需要在自己的Realm中自己實現驗證);
2、提供給SecurityManager來創建Subject(提供身份信息);
MergableAuthenticationInfo用於提供在多Realm時合併AuthenticationInfo的功能,主要合併Principal、如果是其他的如credentialsSalt,會用後邊的信息覆蓋前邊的。
比如HashedCredentialsMatcher,在驗證時會判斷AuthenticationInfo是否是SaltedAuthenticationInfo子類,來獲取鹽信息。
Account相當於我們之前的User,SimpleAccount是其一個實現;在IniRealm、PropertiesRealm這種靜態創建帳號信息的場景中使用,這些Realm直接繼承了SimpleAccountRealm,而SimpleAccountRealm提供了相關的API來動態維護SimpleAccount;即可以通過這些API來動態增刪改查SimpleAccount;動態增刪改查角色/權限信息。及如果您的帳號不是特別多,可以使用這種方式,具體請參考SimpleAccountRealm Javadoc。
其他情況一般返回SimpleAuthenticationInfo即可。
6.4 PrincipalCollection
因爲我們可以在Shiro中同時配置多個Realm,所以呢身份信息可能就有多個;因此其提供了PrincipalCollection用於聚合這些身份信息:
public interface PrincipalCollection extends Iterable, Serializable {
Object getPrimaryPrincipal(); //得到主要的身份
<T> T oneByType(Class<T> type); //根據身份類型獲取第一個
<T> Collection<T> byType(Class<T> type); //根據身份類型獲取一組
List asList(); //轉換爲List
Set asSet(); //轉換爲Set
Collection fromRealm(String realmName); //根據Realm名字獲取
Set<String> getRealmNames(); //獲取所有身份驗證通過的Realm名字
boolean isEmpty(); //判斷是否爲空
}
因爲PrincipalCollection聚合了多個,此處最需要注意的是getPrimaryPrincipal,如果只有一個Principal那麼直接返回即可,如果有多個Principal,則返回第一個(因爲內部使用Map存儲,所以可以認爲是返回任意一個);oneByType / byType根據憑據的類型返回相應的Principal;fromRealm根據Realm名字(每個Principal都與一個Realm關聯)獲取相應的Principal。
MutablePrincipalCollection是一個可變的PrincipalCollection接口,即提供瞭如下可變方法:
public interface MutablePrincipalCollection extends PrincipalCollection {
void add(Object principal, String realmName); //添加Realm-Principal的關聯
void addAll(Collection principals, String realmName); //添加一組Realm-Principal的關聯
void addAll(PrincipalCollection principals);//添加PrincipalCollection
void clear();//清空
}
目前Shiro只提供了一個實現SimplePrincipalCollection,還記得之前的AuthenticationStrategy實現嘛,用於在多Realm時判斷是否滿足條件的,在大多數實現中(繼承了AbstractAuthenticationStrategy)afterAttempt方法會進行AuthenticationInfo(實現了MergableAuthenticationInfo)的merge,比如SimpleAuthenticationInfo會合並多個Principal爲一個PrincipalCollection。
對於PrincipalMap是Shiro 1.2中的一個實驗品,暫時無用,具體可以參考其Javadoc。接下來通過示例來看看PrincipalCollection。
1 、準備三個Realm
MyRealm1
public class MyRealm1 implements Realm {
@Override
public String getName() {
return "a"; //realm name 爲 “a”
}
//省略supports方法,具體請見源碼
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return new SimpleAuthenticationInfo(
"zhang", //身份 字符串類型
"123", //憑據
getName() //Realm Name
);
}
}
MyRealm2
和MyRealm1完全一樣,只是Realm名字爲b。
MyRealm3
public class MyRealm3 implements Realm {
@Override
public String getName() {
return "c"; //realm name 爲 “c”
}
//省略supports方法,具體請見源碼
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
User user = new User("zhang", "123");
return new SimpleAuthenticationInfo(
user, //身份 User類型
"123", //憑據
getName() //Realm Name
);
}
}
和MyRealm1同名,但返回的Principal是User類型。
2、ini配置(shiro-multirealm.ini)
[main] realm1=com.github.zhangkaitao.shiro.chapter6.realm.MyRealm1 realm2=com.github.zhangkaitao.shiro.chapter6.realm.MyRealm2 realm3=com.github.zhangkaitao.shiro.chapter6.realm.MyRealm3 securityManager.realms=$realm1,$realm2,$realm3
3 、測試用例(com.github.zhangkaitao.shiro.chapter6.realm.PrincialCollectionTest )
因爲我們的Realm中沒有進行身份及憑據驗證,所以相當於身份驗證都是成功的,都將返回:
Object primaryPrincipal1 = subject.getPrincipal(); PrincipalCollection princialCollection = subject.getPrincipals(); Object primaryPrincipal2 = princialCollection.getPrimaryPrincipal();
我們可以直接調用subject.getPrincipal獲取PrimaryPrincipal(即所謂的第一個);或者通過getPrincipals獲取PrincipalCollection;然後通過其getPrimaryPrincipal獲取PrimaryPrincipal。
Set<String> realmNames = princialCollection.getRealmNames();
獲取所有身份驗證成功的Realm名字。
Set<Object> principals = princialCollection.asSet(); //asList和asSet的結果一樣
將身份信息轉換爲Set/List,即使轉換爲List,也是先轉換爲Set再完成的。
Collection<User> users = princialCollection.fromRealm("c");
根據Realm名字獲取身份,因爲Realm名字可以重複,所以可能多個身份,建議Realm名字儘量不要重複。
6.4 AuthorizationInfo
AuthorizationInfo用於聚合授權信息的:
public interface AuthorizationInfo extends Serializable {
Collection<String> getRoles(); //獲取角色字符串信息
Collection<String> getStringPermissions(); //獲取權限字符串信息
Collection<Permission> getObjectPermissions(); //獲取Permission對象信息
}
當我們使用AuthorizingRealm時,如果身份驗證成功,在進行授權時就通過doGetAuthorizationInfo方法獲取角色/權限信息用於授權驗證。
Shiro提供了一個實現SimpleAuthorizationInfo,大多數時候使用這個即可。
對於Account及SimpleAccount,之前的【6.3 AuthenticationInfo】已經介紹過了,用於SimpleAccountRealm子類,實現動態角色/權限維護的。
6.5 Subject
Subject是Shiro的核心對象,基本所有身份驗證、授權都是通過Subject完成。
1 、身份信息獲取
Object getPrincipal(); //Primary Principal PrincipalCollection getPrincipals(); // PrincipalCollection
2 、身份驗證
void login(AuthenticationToken token) throws AuthenticationException;
boolean isAuthenticated();
boolean isRemembered();
通過login登錄,如果登錄失敗將拋出相應的AuthenticationException,如果登錄成功調用isAuthenticated就會返回true,即已經通過身份驗證;如果isRemembered返回true,表示是通過記住我功能登錄的而不是調用login方法登錄的。isAuthenticated/isRemembered是互斥的,即如果其中一個返回true,另一個返回false。
3 、角色授權驗證
boolean hasRole(String roleIdentifier); boolean[] hasRoles(List<String> roleIdentifiers); boolean hasAllRoles(Collection<String> roleIdentifiers); void checkRole(String roleIdentifier) throws AuthorizationException; void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException; void checkRoles(String... roleIdentifiers) throws AuthorizationException;
hasRole*進行角色驗證,驗證後返回true/false;而checkRole*驗證失敗時拋出AuthorizationException異常。
4 、權限授權驗證
boolean isPermitted(String permission);
boolean isPermitted(Permission permission);
boolean[] isPermitted(String... permissions);
boolean[] isPermitted(List<Permission> permissions);
boolean isPermittedAll(String... permissions);
boolean isPermittedAll(Collection<Permission> permissions);
void checkPermission(String permission) throws AuthorizationException;
void checkPermission(Permission permission) throws AuthorizationException;
void checkPermissions(String... permissions) throws AuthorizationException;
void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;
isPermitted*進行權限驗證,驗證後返回true/false;而checkPermission*驗證失敗時拋出AuthorizationException。
5 、會話
Session getSession(); //相當於getSession(true)
Session getSession(boolean create);
類似於Web中的會話。如果登錄成功就相當於建立了會話,接着可以使用getSession獲取;如果create=true如果沒有會話將返回null,而create=true如果沒有會話會強制創建一個。
6 、退出
void logout();
7 、RunAs
void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException;
boolean isRunAs();
PrincipalCollection getPreviousPrincipals();
PrincipalCollection releaseRunAs();
RunAs即實現“允許A假設爲B身份進行訪問”;通過調用subject.runAs(b)進行訪問;接着調用subject.getPrincipals將獲取到B的身份;此時調用isRunAs將返回true;而a的身份需要通過subject. getPreviousPrincipals獲取;如果不需要RunAs了調用subject. releaseRunAs即可。
8 、多線程
<V> V execute(Callable<V> callable) throws ExecutionException; void execute(Runnable runnable); <V> Callable<V> associateWith(Callable<V> callable); Runnable associateWith(Runnable runnable);
實現線程之間的Subject傳播,因爲Subject是線程綁定的;因此在多線程執行中需要傳播到相應的線程才能獲取到相應的Subject。最簡單的辦法就是通過execute(runnable/callable實例)直接調用;或者通過associateWith(runnable/callable實例)得到一個包裝後的實例;它們都是通過:1、把當前線程的Subject綁定過去;2、在線程執行結束後自動釋放。
Subject自己不會實現相應的身份驗證/授權邏輯,而是通過DelegatingSubject委託給SecurityManager實現;及可以理解爲Subject是一個面門。
對於Subject的構建一般沒必要我們去創建;一般通過SecurityUtils.getSubject()獲取:
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
即首先查看當前線程是否綁定了Subject,如果沒有通過Subject.Builder構建一個然後綁定到現場返回。
如果想自定義創建,可以通過:
new Subject.Builder().principals(身份).authenticated(true/false).buildSubject()
這種可以創建相應的Subject實例了,然後自己綁定到線程即可。在new Builder()時如果沒有傳入SecurityManager,自動調用SecurityUtils.getSecurityManager獲取;也可以自己傳入一個實例。
對於Subject我們一般這麼使用:
1、身份驗證(login)
2、授權(hasRole*/isPermitted*或checkRole*/checkPermission*)
3、將相應的數據存儲到會話(Session)
4、切換身份(RunAs)/多線程身份傳播
5、退出
而我們必須的功能就是1、2、5。到目前爲止我們就可以使用Shiro進行應用程序的安全控制了,但是還是缺少如對Web驗證、Java方法驗證等的一些簡化實現。