1. 程式人生 > >第六章----- Realm及相關物件

第六章----- Realm及相關物件

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

Java程式碼  收藏程式碼
  1. public interface PermissionService {  
  2.     public Permission createPermission(Permission permission);  
  3.     public void deletePermission(Long permissionId);  
  4. }  

實現基本的建立/刪除許可權。

RoleService 

Java程式碼  收藏程式碼
  1. public interface RoleService {  
  2.     public Role createRole(Role role);  
  3.     public void deleteRole(Long roleId);  
  4.     //新增角色-許可權之間關係  
  5.     public void correlationPermissions(Long roleId, Long... permissionIds);  
  6.     //移除角色-許可權之間關係  
  7.     public void uncorrelationPermissions(Long roleId, Long... permissionIds);//  
  8. }   

相對於PermissionService多了關聯/移除關聯角色-許可權功能。

UserService 

Java程式碼  收藏程式碼
  1. public interface UserService {  
  2.     public User createUser(User user); //建立賬戶  
  3.     public void changePassword(Long userId, String newPassword);//修改密碼  
  4.     public void correlationRoles(Long userId, Long... roleIds); //新增使用者-角色關係  
  5.     public void uncorrelationRoles(Long userId, Long... roleIds);// 移除使用者-角色關係  
  6.     public User findByUsername(String username);// 根據使用者名稱查詢使用者  
  7.     public Set<String> findRoles(String username);// 根據使用者名稱查詢其角色  
  8.     public Set<String> findPermissions(String username); //根據使用者名稱查詢其許可權  
  9. }   

此處使用findByUsername、findRoles及findPermissions來查詢使用者名稱對應的帳號、角色及許可權資訊。之後的Realm就使用這些方法來查詢相關資訊。

UserServiceImpl  

Java程式碼  收藏程式碼
  1. public User createUser(User user) {  
  2.     //加密密碼  
  3.     passwordHelper.encryptPassword(user);  
  4.     return userDao.createUser(user);  
  5. }  
  6. public void changePassword(Long userId, String newPassword) {  
  7.     User user =userDao.findOne(userId);  
  8.     user.setPassword(newPassword);  
  9.     passwordHelper.encryptPassword(user);  
  10.     userDao.updateUser(user);  
  11. }   

在建立賬戶及修改密碼時直接把生成密碼操作委託給PasswordHelper。

PasswordHelper

Java程式碼  收藏程式碼
  1. public class PasswordHelper {  
  2.     private RandomNumberGenerator randomNumberGenerator =  
  3.      new SecureRandomNumberGenerator();  
  4.     private String algorithmName = "md5";  
  5.     private final int hashIterations = 2;  
  6.     public void encryptPassword(User user) {  
  7.         user.setSalt(randomNumberGenerator.nextBytes().toHex());  
  8.         String newPassword = new SimpleHash(  
  9.                 algorithmName,  
  10.                 user.getPassword(),  
  11.                 ByteSource.Util.bytes(user.getCredentialsSalt()),  
  12.                 hashIterations).toHex();  
  13.         user.setPassword(newPassword);  
  14.     }  
  15. }   

之後的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。 

Java程式碼  收藏程式碼
  1. public class UserRealm extends AuthorizingRealm {  
  2.     private UserService userService = new UserServiceImpl();  
  3.     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {  
  4.         String username = (String)principals.getPrimaryPrincipal();  
  5.         SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();  
  6.         authorizationInfo.setRoles(userService.findRoles(username));  
  7.         authorizationInfo.setStringPermissions(userService.findPermissions(username));  
  8.         return authorizationInfo;  
  9.     }  
  10.     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
  11.         String username = (String)token.getPrincipal();  
  12.         User user = userService.findByUsername(username);  
  13.         if(user == null) {  
  14.             throw new UnknownAccountException();//沒找到帳號  
  15.         }  
  16.         if(Boolean.TRUE.equals(user.getLocked())) {  
  17.             throw new LockedAccountException(); //帳號鎖定  
  18.         }  
  19.         //交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,如果覺得人家的不好可以在此判斷或自定義實現  
  20.         SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(  
  21.                 user.getUsername(), //使用者名稱  
  22.                 user.getPassword(), //密碼  
  23.                 ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt  
  24.                 getName()  //realm name  
  25.         );  
  26.         return authenticationInfo;  
  27.     }  
  28. }   

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用於收集使用者提交的身份(如使用者名稱)及憑據(如密碼):

Java程式碼  收藏程式碼
  1. public interface AuthenticationToken extends Serializable {  
  2.     Object getPrincipal(); //身份  
  3.     Object getCredentials(); //憑據  
  4. }   

擴充套件介面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用於聚合這些身份資訊:

Java程式碼  收藏程式碼
  1. public interface PrincipalCollection extends Iterable, Serializable {  
  2.     Object getPrimaryPrincipal(); //得到主要的身份  
  3.     <T> T oneByType(Class<T> type); //根據身份型別獲取第一個  
  4.     <T> Collection<T> byType(Class<T> type); //根據身份型別獲取一組  
  5.     List asList(); //轉換為List  
  6.     Set asSet(); //轉換為Set  
  7.     Collection fromRealm(String realmName); //根據Realm名字獲取  
  8.     Set<String> getRealmNames(); //獲取所有身份驗證通過的Realm名字  
  9.     boolean isEmpty(); //判斷是否為空  
  10. }   

因為PrincipalCollection聚合了多個,此處最需要注意的是getPrimaryPrincipal,如果只有一個Principal那麼直接返回即可,如果有多個Principal,則返回第一個(因為內部使用Map儲存,所以可以認為是返回任意一個);oneByType / byType根據憑據的型別返回相應的Principal;fromRealm根據Realm名字(每個Principal都與一個Realm關聯)獲取相應的Principal。

MutablePrincipalCollection是一個可變的PrincipalCollection介面,即提供瞭如下可變方法:

Java程式碼  收藏程式碼
  1. public interface MutablePrincipalCollection extends PrincipalCollection {  
  2.     void add(Object principal, String realmName); //新增Realm-Principal的關聯  
  3.     void addAll(Collection principals, String realmName); //新增一組Realm-Principal的關聯  
  4.     void addAll(PrincipalCollection principals);//新增PrincipalCollection  
  5.     void clear();//清空  
  6. }   

目前Shiro只提供了一個實現SimplePrincipalCollection,還記得之前的AuthenticationStrategy實現嘛,用於在多Realm時判斷是否滿足條件的,在大多數實現中(繼承了AbstractAuthenticationStrategy)afterAttempt方法會進行AuthenticationInfo(實現了MergableAuthenticationInfo)的merge,比如SimpleAuthenticationInfo會合並多個Principal為一個PrincipalCollection。

對於PrincipalMap是Shiro 1.2中的一個實驗品,暫時無用,具體可以參考其Javadoc。接下來通過示例來看看PrincipalCollection。

1、準備三個Realm

MyRealm1

Java程式碼  收藏程式碼
  1. public class MyRealm1 implements Realm {  
  2.     @Override  
  3.     public String getName() {  
  4.         return "a"//realm name 為 “a”  
  5.     }  
  6.     //省略supports方法,具體請見原始碼  
  7.     @Override  
  8.     public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
  9.         return new SimpleAuthenticationInfo(  
  10.                 "zhang"//身份 字串型別  
  11.                 "123",   //憑據  
  12.                 getName() //Realm Name  
  13.         );  
  14.     }  
  15. }  

MyRealm2 

和MyRealm1完全一樣,只是Realm名字為b。

MyRealm3

Java程式碼  收藏程式碼
  1. public class MyRealm3 implements Realm {  
  2.     @Override  
  3.     public String getName() {  
  4.         return "c"//realm name 為 “c”  
  5.     }  
  6.     //省略supports方法,具體請見原始碼  
  7.     @Override  
  8.     public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
  9.         User user = new User("zhang""123");  
  10.         return new SimpleAuthenticationInfo(  
  11.                 user, //身份 User型別  
  12.                 "123",   //憑據  
  13.                 getName() //Realm Name  
  14.         );  
  15.     }  
  16. }   

和MyRealm1同名,但返回的Principal是User型別。

2、ini配置(shiro-multirealm.ini)

Java程式碼  收藏程式碼
  1. [main]  
  2. realm1=com.github.zhangkaitao.shiro.chapter6.realm.MyRealm1  
  3. realm2=com.github.zhangkaitao.shiro.chapter6.realm.MyRealm2  
  4. realm3=com.github.zhangkaitao.shiro.chapter6.realm.MyRealm3  
  5. securityManager.realms=$realm1,$realm2,$realm3   

3、測試用例(com.github.zhangkaitao.shiro.chapter6.realm.PrincialCollectionTest)

因為我們的Realm中沒有進行身份及憑據驗證,所以相當於身份驗證都是成功的,都將返回:

Java程式碼  收藏程式碼
  1. Object primaryPrincipal1 = subject.getPrincipal();  
  2. PrincipalCollection princialCollection = subject.getPrincipals();  
  3. Object primaryPrincipal2 = princialCollection.getPrimaryPrincipal();   

我們可以直接呼叫subject.getPrincipal獲取PrimaryPrincipal(即所謂的第一個);或者通過getPrincipals獲取PrincipalCollection;然後通過其getPrimaryPrincipal獲取PrimaryPrincipal。

Java程式碼  收藏程式碼
  1. Set<String> realmNames = princialCollection.getRealmNames();  

獲取所有身份驗證成功的Realm名字。      

Java程式碼  收藏程式碼
  1. Set<Object> principals = princialCollection.asSet(); //asList和asSet的結果一樣  

將身份資訊轉換為Set/List,即使轉換為List,也是先轉換為Set再完成的。

Java程式碼  收藏程式碼
  1. Collection<User> users = princialCollection.fromRealm("c");  

根據Realm名字獲取身份,因為Realm名字可以重複,所以可能多個身份,建議Realm名字儘量不要重複。

6.4 AuthorizationInfo

AuthorizationInfo用於聚合授權資訊的:

Java程式碼  收藏程式碼
  1. public interface AuthorizationInfo extends Serializable {  
  2.     Collection<String> getRoles(); //獲取角色字串資訊  
  3.     Collection<String> getStringPermissions(); //獲取許可權字串資訊  
  4.     Collection<Permission> getObjectPermissions(); //獲取Permission物件資訊  
  5. }   

當我們使用AuthorizingRealm時,如果身份驗證成功,在進行授權時就通過doGetAuthorizationInfo方法獲取角色/許可權資訊用於授權驗證。

Shiro提供了一個實現SimpleAuthorizationInfo,大多數時候使用這個即可。

對於Account及SimpleAccount,之前的【6.3 AuthenticationInfo】已經介紹過了,用於SimpleAccountRealm子類,實現動態角色/許可權維護的。

6.5 Subject

Subject是Shiro的核心物件,基本所有身份驗證、授權都是通過Subject完成。

1、身份資訊獲取

Java程式碼  收藏程式碼
  1. Object getPrincipal(); //Primary Principal  
  2. PrincipalCollection getPrincipals(); // PrincipalCollection   

2、身份驗證

Java程式碼  收藏程式碼
  1. void login(AuthenticationToken token) throws AuthenticationException;  
  2. boolean isAuthenticated();  
  3. boolean isRemembered();  

通過login登入,如果登入失敗將丟擲相應的AuthenticationException,如果登入成功呼叫isAuthenticated就會返回true,即已經通過身份驗證;如果isRemembered返回true,表示是通過記住我功能登入的而不是呼叫login方法登入的。isAuthenticated/isRemembered是互斥的,即如果其中一個返回true,另一個返回false。

3、角色授權驗證 

Java程式碼  收藏程式碼
  1. boolean hasRole(String roleIdentifier);  
  2. boolean[] hasRoles(List<String> roleIdentifiers);  
  3. boolean hasAllRoles(Collection<String> roleIdentifiers);  
  4. void checkRole(String roleIdentifier) throws AuthorizationException;  
  5. void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException;  
  6. void checkRoles(String... roleIdentifiers) throws AuthorizationException;   

hasRole*進行角色驗證,驗證後返回true/false;而checkRole*驗證失敗時丟擲AuthorizationException異常。 

4、許可權授權驗證

Java程式碼  收藏程式碼
  1. boolean isPermitted(String permission);  
  2. boolean isPermitted(Permission permission);  
  3. boolean[] isPermitted(String... permissions);  
  4. boolean[] isPermitted(List<Permission> permissions);  
  5. boolean isPermittedAll(String... permissions);  
  6. boolean isPermittedAll(Collection<Permission> permissions);  
  7. void checkPermission(String permission) throws AuthorizationException;  
  8. void checkPermission(Permission permission) throws AuthorizationException;  
  9. void checkPermissions(String... permissions) throws AuthorizationException;  
  10. void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;  

isPermitted*進行許可權驗證,驗證後返回true/false;而checkPermission*驗證失敗時丟擲AuthorizationException。

5、會話

Java程式碼  收藏程式碼
  1. Session getSession(); //相當於getSession(true)  
  2. Session getSession(boolean create);    

類似於Web中的會話。如果登入成功就相當於建立了會話,接著可以使用getSession獲取;如果create=false如果沒有會話將返回null,而create=true如果沒有會話會強制建立一個。

6、退出 

Java程式碼  收藏程式碼
  1. void logout();  

7、RunAs  

Java程式碼  收藏程式碼
  1. void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException;  
  2. boolean isRunAs();  
  3. PrincipalCollection getPreviousPrincipals();  
  4. PrincipalCollection releaseRunAs();   

RunAs即實現“允許A假設為B身份進行訪問”;通過呼叫subject.runAs(b)進行訪問;接著呼叫subject.getPrincipals將獲取到B的身份;此時呼叫isRunAs將返回true;而a的身份需要通過subject. getPreviousPrincipals獲取;如果不需要RunAs了呼叫subject. releaseRunAs即可。

8、多執行緒

Java程式碼  收藏程式碼
  1. <V> V execute(Callable<V> callable) throws ExecutionException;  
  2. void execute(Runnable runnable);  
  3. <V> Callable<V> associateWith(Callable<V> callable);  
  4. Runnable associateWith(Runnable runnable);   

實現執行緒之間的Subject傳播,因為Subject是執行緒繫結的;因此在多執行緒執行中需要傳播到相應的執行緒才能獲取到相應的Subject。最簡單的辦法就是通過execute(runnable/callable例項)直接呼叫;或者通過associateWith(runnable/callable例項)得到一個包裝後的例項;它們都是通過:1、把當前執行緒的Subject繫結過去;2、線上程執行結束後自動釋放。

Subject自己不會實現相應的身份驗證/授權邏輯,而是通過DelegatingSubject委託給SecurityManager實現;及可以理解為Subject是一個面門。

對於Subject的構建一般沒必要我們去建立;一般通過SecurityUtils.getSubject()獲取:

Java程式碼  收藏程式碼
  1. public static Subject getSubject() {  
  2.     Subject subject = ThreadContext.getSubject();  
  3.     if (subject == null) {  
  4.         subject = (new Subject.Builder()).buildSubject();  
  5.         ThreadContext.bind(subject);  
  6.     }  
  7.     return subject;  
  8. }   

即首先檢視當前執行緒是否綁定了Subject,如果沒有通過Subject.Builder構建一個然後繫結到現場返回。

如果想自定義建立,可以通過:

Java程式碼  收藏程式碼
  1. 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方法驗證等的