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代碼??
public?interface?PermissionService?{??????public?Permission?createPermission(Permission?permission);??????public?void?deletePermission(Long?permissionId);??}??
實現基本的創建/刪除權限。
?
RoleService?
Java代碼??
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?
Java代碼??
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??
Java代碼??
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
Java代碼??
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。?
Java代碼??
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用于收集用戶提交的身份(如用戶名)及憑據(如密碼):
Java代碼??
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用于聚合這些身份信息:
Java代碼??
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接口,即提供了如下可變方法:
Java代碼??
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
Java代碼??
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
Java代碼??
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)
Java代碼??
[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中沒有進行身份及憑據驗證,所以相當于身份驗證都是成功的,都將返回:
Java代碼??
Object?primaryPrincipal1?=?subject.getPrincipal();??PrincipalCollection?princialCollection?=?subject.getPrincipals();??Object?primaryPrincipal2?=?princialCollection.getPrimaryPrincipal();???
我們可以直接調用subject.getPrincipal獲取PrimaryPrincipal(即所謂的第一個);或者通過getPrincipals獲取PrincipalCollection;然后通過其getPrimaryPrincipal獲取PrimaryPrincipal。
?
Java代碼??
Set<String>?realmNames?=?princialCollection.getRealmNames();??
獲取所有身份驗證成功的Realm名字。??????
?
Java代碼??
Set<Object>?principals?=?princialCollection.asSet();?//asList和asSet的結果一樣??
將身份信息轉換為Set/List,即使轉換為List,也是先轉換為Set再完成的。
?
Java代碼??
Collection<User>?users?=?princialCollection.fromRealm("c");??
根據Realm名字獲取身份,因為Realm名字可以重復,所以可能多個身份,建議Realm名字盡量不要重復。
?
6.4 AuthorizationInfo
AuthorizationInfo用于聚合授權信息的:
Java代碼??
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、身份信息獲取
Java代碼??
Object?getPrincipal();?//Primary?Principal??PrincipalCollection?getPrincipals();?//?PrincipalCollection???
?
2、身份驗證
Java代碼??
void?login(AuthenticationToken?token)?throws?AuthenticationException;??boolean?isAuthenticated();??boolean?isRemembered();??
通過login登錄,如果登錄失敗將拋出相應的AuthenticationException,如果登錄成功調用isAuthenticated就會返回true,即已經通過身份驗證;如果isRemembered返回true,表示是通過記住我功能登錄的而不是調用login方法登錄的。isAuthenticated/isRemembered是互斥的,即如果其中一個返回true,另一個返回false。
??
3、角色授權驗證?
Java代碼??
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、權限授權驗證
Java代碼??
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、會話
Java代碼??
Session?getSession();?//相當于getSession(true)??Session?getSession(boolean?create);????
類似于Web中的會話。如果登錄成功就相當于建立了會話,接著可以使用getSession獲取;如果create=false如果沒有會話將返回null,而create=true如果沒有會話會強制創建一個。
?
6、退出?
Java代碼??
void?logout();??
?
7、RunAs??
Java代碼??
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、多線程
Java代碼??
<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()獲取:
Java代碼??
public?static?Subject?getSubject()?{??????Subject?subject?=?ThreadContext.getSubject();??????if?(subject?==?null)?{??????????subject?=?(new?Subject.Builder()).buildSubject();??????????ThreadContext.bind(subject);??????}??????return?subject;??}???
即首先查看當前線程是否綁定了Subject,如果沒有通過Subject.Builder構建一個然后綁定到現場返回。
?
如果想自定義創建,可以通過:
Java代碼??
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方法驗證等的一些簡化實現。 ? ?
?
示例源代碼:https://github.com/zhangkaitao/shiro-example;
總結
以上是生活随笔為你收集整理的Realm及相关对象——《跟我学Shiro》的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。