【Realm】及【Authorizer】部分都已經(jīng)詳細(xì)介紹過(guò) Realm 了,接下來(lái)再來(lái)看一下一般真實(shí)環(huán)境下的 Realm 如何實(shí)現(xiàn)。
1、定義實(shí)體及關(guān)系
即用戶 - 角色之間是多對(duì)多關(guān)系,角色 - 權(quán)限之間是多對(duì)多關(guān)系;且用戶和權(quán)限之間通過(guò)角色建立關(guān)系;在系統(tǒng)中驗(yàn)證時(shí)通過(guò)權(quán)限驗(yàn)證,角色只是權(quán)限集合,即所謂的顯示角色;其實(shí)權(quán)限應(yīng)該對(duì)應(yīng)到資源(如菜單、URL、頁(yè)面按鈕、Java 方法等)中,即應(yīng)該將權(quán)限字符串存儲(chǔ)到資源實(shí)體中,但是目前為了簡(jiǎn)單化,直接提取一個(gè)權(quán)限表,【綜合示例】部分會(huì)使用完整的表結(jié)構(gòu)。
用戶實(shí)體包括:編號(hào) (id)、用戶名 (username)、密碼 (password)、鹽 (salt)、是否鎖定 (locked);是否鎖定用于封禁用戶使用,其實(shí)最好使用 Enum 字段存儲(chǔ),可以實(shí)現(xiàn)更復(fù)雜的用戶狀態(tài)實(shí)現(xiàn)。 角色實(shí)體包括:、編號(hào) (id)、角色標(biāo)識(shí)符(role)、描述(description)、是否可用(available);其中角色標(biāo)識(shí)符用于在程序中進(jìn)行隱式角色判斷的,描述用于以后再前臺(tái)界面顯示的、是否可用表示角色當(dāng)前是否激活。 權(quán)限實(shí)體包括:編號(hào)(id)、權(quán)限標(biāo)識(shí)符(permission)、描述(description)、是否可用(available);含義和角色實(shí)體類似不再闡述。
另外還有兩個(gè)關(guān)系實(shí)體:用戶 - 角色實(shí)體(用戶編號(hào)、角色編號(hào),且組合為復(fù)合主鍵);角色 - 權(quán)限實(shí)體(角色編號(hào)、權(quán)限編號(hào),且組合為復(fù)合主鍵)。
sql 及實(shí)體請(qǐng)參考源代碼中的 sql\shiro.sql 和 com.github.zhangkaitao.shiro.chapter6.entity 對(duì)應(yīng)的實(shí)體。
Shiro 中 Realm 的繼承結(jié)構(gòu)如下:
2、環(huán)境準(zhǔn)備
為了方便數(shù)據(jù)庫(kù)操作,使用了 “org.springframework: spring-jdbc: 4.0.0.RELEASE” 依賴,雖然是 spring4 版本的,但使用上和 spring3 無(wú)區(qū)別。其他依賴請(qǐng)參考源碼的 pom.xml。
3、定義 Service 及 Dao
為了實(shí)現(xiàn)的簡(jiǎn)單性,只實(shí)現(xiàn)必須的功能,其他的可以自己實(shí)現(xiàn)即可。
PermissionService
public interface PermissionService {
public Permission createPermission(Permission permission);
public void deletePermission(Long permissionId);
}
實(shí)現(xiàn)基本的創(chuàng)建 / 刪除權(quán)限。
RoleService
public interface RoleService {
public Role createRole(Role role);
public void deleteRole(Long roleId);
//添加角色-權(quán)限之間關(guān)系
public void correlationPermissions(Long roleId, Long... permissionIds);
//移除角色-權(quán)限之間關(guān)系
public void uncorrelationPermissions(Long roleId, Long... permissionIds);//
}
相對(duì)于 PermissionService 多了關(guān)聯(lián) / 移除關(guān)聯(lián)角色 - 權(quán)限功能。
UserService
public interface UserService {
public User createUser(User user); //創(chuàng)建賬戶
public void changePassword(Long userId, String newPassword);//修改密碼
public void correlationRoles(Long userId, Long... roleIds); //添加用戶-角色關(guān)系
public void uncorrelationRoles(Long userId, Long... roleIds);// 移除用戶-角色關(guān)系
public User findByUsername(String username);// 根據(jù)用戶名查找用戶
public Set<String> findRoles(String username);// 根據(jù)用戶名查找其角色
public Set<String> findPermissions(String username); //根據(jù)用戶名查找其權(quán)限
}
此處使用 findByUsername、findRoles 及 findPermissions 來(lái)查找用戶名對(duì)應(yīng)的帳號(hào)、角色及權(quán)限信息。之后的 Realm 就使用這些方法來(lái)查找相關(guān)信息。
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);
}
在創(chuàng)建賬戶及修改密碼時(shí)直接把生成密碼操作委托給 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。
為了節(jié)省篇幅,對(duì)于 DAO/Service 的接口及實(shí)現(xiàn),具體請(qǐng)參考源碼com.github.zhangkaitao.shiro.chapter6。另外請(qǐng)參考 Service 層的測(cè)試用例 com.github.zhangkaitao.shiro.chapter6.service.ServiceTest。
4、定義 Realm
RetryLimitHashedCredentialsMatcher
和第五章的一樣,在此就不羅列代碼了,請(qǐng)參考源碼 com.github.zhangkaitao.shiro.chapter6.credentials.RetryLimitHashedCredentialsMatcher。
UserRealm
另外請(qǐng)參考 Service 層的測(cè)試用例 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();//沒(méi)找到帳號(hào)
}
if(Boolean.TRUE.equals(user.getLocked())) {
throw new LockedAccountException(); //帳號(hào)鎖定
}
//交給AuthenticatingRealm使用CredentialsMatcher進(jìn)行密碼匹配,如果覺(jué)得人家的不好可以在此判斷或自定義實(shí)現(xiàn)
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 相關(guān)信息分成兩步:獲取身份驗(yàn)證信息(doGetAuthenticationInfo)及授權(quán)信息(doGetAuthorizationInfo);
2、doGetAuthenticationInfo 獲取身份驗(yàn)證相關(guān)信息:首先根據(jù)傳入的用戶名獲取 User 信息;然后如果 user 為空,那么拋出沒(méi)找到帳號(hào)異常 UnknownAccountException;如果 user 找到但鎖定了拋出鎖定異常 LockedAccountException;最后生成 AuthenticationInfo 信息,交給間接父類 AuthenticatingRealm 使用 CredentialsMatcher 進(jìn)行判斷密碼是否匹配,如果不匹配將拋出密碼錯(cuò)誤異常 IncorrectCredentialsException;另外如果密碼重試此處太多將拋出超出重試次數(shù)異常 ExcessiveAttemptsException;在組裝 SimpleAuthenticationInfo 信息時(shí),需要傳入:身份信息(用戶名)、憑據(jù)(密文密碼)、鹽(username+salt),CredentialsMatcher 使用鹽加密傳入的明文密碼和此處的密文密碼進(jìn)行匹配。
3、doGetAuthorizationInfo 獲取授權(quán)信息:PrincipalCollection 是一個(gè)身份集合,因?yàn)槲覀儸F(xiàn)在就一個(gè) Realm,所以直接調(diào)用 getPrimaryPrincipal 得到之前傳入的用戶名即可;然后根據(jù)用戶名調(diào)用 UserService 接口獲取角色及權(quán)限信息。
5、測(cè)試用例
為了節(jié)省篇幅,請(qǐng)參考測(cè)試用例 com.github.zhangkaitao.shiro.chapter6.realm.UserRealmTest。包含了:登錄成功、用戶名錯(cuò)誤、密碼錯(cuò)誤、密碼超出重試次數(shù)、有 / 沒(méi)有角色、有 / 沒(méi)有權(quán)限的測(cè)試。
AuthenticationToken 用于收集用戶提交的身份(如用戶名)及憑據(jù)(如密碼):
public interface AuthenticationToken extends Serializable {
Object getPrincipal(); //身份
Object getCredentials(); //憑據(jù)
}
擴(kuò)展接口 RememberMeAuthenticationToken:提供了 “boolean isRememberMe()” 現(xiàn)“記住我”的功能; 擴(kuò)展接口是 HostAuthenticationToken:提供了 “String getHost()” 方法用于獲取用戶 “主機(jī)” 的功能。
Shiro 提供了一個(gè)直接拿來(lái)用的 UsernamePasswordToken,用于實(shí)現(xiàn)用戶名 / 密碼 Token 組,另外其實(shí)現(xiàn)了 RememberMeAuthenticationToken 和 HostAuthenticationToken,可以實(shí)現(xiàn)記住我及主機(jī)驗(yàn)證的支持。
AuthenticationInfo 有兩個(gè)作用:
MergableAuthenticationInfo 用于提供在多 Realm 時(shí)合并 AuthenticationInfo 的功能,主要合并 Principal、如果是其他的如 credentialsSalt,會(huì)用后邊的信息覆蓋前邊的。
比如 HashedCredentialsMatcher,在驗(yàn)證時(shí)會(huì)判斷 AuthenticationInfo 是否是 SaltedAuthenticationInfo 子類,來(lái)獲取鹽信息。
Account 相當(dāng)于我們之前的 User,SimpleAccount 是其一個(gè)實(shí)現(xiàn);在 IniRealm、PropertiesRealm 這種靜態(tài)創(chuàng)建帳號(hào)信息的場(chǎng)景中使用,這些 Realm 直接繼承了 SimpleAccountRealm,而 SimpleAccountRealm 提供了相關(guān)的 API 來(lái)動(dòng)態(tài)維護(hù) SimpleAccount;即可以通過(guò)這些 API 來(lái)動(dòng)態(tài)增刪改查 SimpleAccount;動(dòng)態(tài)增刪改查角色 / 權(quán)限信息。及如果您的帳號(hào)不是特別多,可以使用這種方式,具體請(qǐng)參考 SimpleAccountRealm Javadoc。
其他情況一般返回 SimpleAuthenticationInfo 即可。
因?yàn)槲覀兛梢栽?Shiro 中同時(shí)配置多個(gè) Realm,所以呢身份信息可能就有多個(gè);因此其提供了 PrincipalCollection 用于聚合這些身份信息:
public interface PrincipalCollection extends Iterable, Serializable {
Object getPrimaryPrincipal(); //得到主要的身份
<T> T oneByType(Class<T> type); //根據(jù)身份類型獲取第一個(gè)
<T> Collection<T> byType(Class<T> type); //根據(jù)身份類型獲取一組
List asList(); //轉(zhuǎn)換為L(zhǎng)ist
Set asSet(); //轉(zhuǎn)換為Set
Collection fromRealm(String realmName); //根據(jù)Realm名字獲取
Set<String> getRealmNames(); //獲取所有身份驗(yàn)證通過(guò)的Realm名字
boolean isEmpty(); //判斷是否為空
}
因?yàn)?PrincipalCollection 聚合了多個(gè),此處最需要注意的是 getPrimaryPrincipal,如果只有一個(gè) Principal 那么直接返回即可,如果有多個(gè) Principal,則返回第一個(gè)(因?yàn)閮?nèi)部使用 Map 存儲(chǔ),所以可以認(rèn)為是返回任意一個(gè));oneByType / byType 根據(jù)憑據(jù)的類型返回相應(yīng)的 Principal;fromRealm 根據(jù) Realm 名字(每個(gè) Principal 都與一個(gè) Realm 關(guān)聯(lián))獲取相應(yīng)的 Principal。
MutablePrincipalCollection 是一個(gè)可變的 PrincipalCollection 接口,即提供了如下可變方法:
public interface MutablePrincipalCollection extends PrincipalCollection {
void add(Object principal, String realmName); //添加Realm-Principal的關(guān)聯(lián)
void addAll(Collection principals, String realmName); //添加一組Realm-Principal的關(guān)聯(lián)
void addAll(PrincipalCollection principals);//添加PrincipalCollection
void clear();//清空
}
目前 Shiro 只提供了一個(gè)實(shí)現(xiàn) SimplePrincipalCollection,還記得之前的 AuthenticationStrategy 實(shí)現(xiàn)嘛,用于在多 Realm 時(shí)判斷是否滿足條件的,在大多數(shù)實(shí)現(xiàn)中(繼承了 AbstractAuthenticationStrategy)afterAttempt 方法會(huì)進(jìn)行 AuthenticationInfo(實(shí)現(xiàn)了 MergableAuthenticationInfo)的 merge,比如 SimpleAuthenticationInfo 會(huì)合并多個(gè) Principal 為一個(gè) PrincipalCollection。
對(duì)于 PrincipalMap 是 Shiro 1.2 中的一個(gè)實(shí)驗(yàn)品,暫時(shí)無(wú)用,具體可以參考其 Javadoc。接下來(lái)通過(guò)示例來(lái)看看 PrincipalCollection。
1、準(zhǔn)備三個(gè) Realm
MyRealm1
public class MyRealm1 implements Realm {
@Override
public String getName() {
return "a"; //realm name 為 “a”
}
//省略supports方法,具體請(qǐng)見(jiàn)源碼
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return new SimpleAuthenticationInfo(
"zhang", //身份 字符串類型
"123", //憑據(jù)
getName() //Realm Name
);
}
}
MyRealm2
和 MyRealm1 完全一樣,只是 Realm 名字為 b。
MyRealm3
public class MyRealm3 implements Realm {
@Override
public String getName() {
return "c"; //realm name 為 “c”
}
//省略supports方法,具體請(qǐng)見(jiàn)源碼
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
User user = new User("zhang", "123");
return new SimpleAuthenticationInfo(
user, //身份 User類型
"123", //憑據(jù)
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、測(cè)試用例(com.github.zhangkaitao.shiro.chapter6.realm.PrincialCollectionTest)
因?yàn)槲覀兊?Realm 中沒(méi)有進(jìn)行身份及憑據(jù)驗(yàn)證,所以相當(dāng)于身份驗(yàn)證都是成功的,都將返回:
Object primaryPrincipal1 = subject.getPrincipal();
PrincipalCollection princialCollection = subject.getPrincipals();
Object primaryPrincipal2 = princialCollection.getPrimaryPrincipal();
我們可以直接調(diào)用 subject.getPrincipal 獲取 PrimaryPrincipal(即所謂的第一個(gè));或者通過(guò) getPrincipals 獲取 PrincipalCollection;然后通過(guò)其 getPrimaryPrincipal 獲取 PrimaryPrincipal。
Set<String> realmNames = princialCollection.getRealmNames();
獲取所有身份驗(yàn)證成功的 Realm 名字。
Set<Object> principals = princialCollection.asSet(); //asList 和 asSet 的結(jié)果一樣
將身份信息轉(zhuǎn)換為 Set/List,即使轉(zhuǎn)換為 List,也是先轉(zhuǎn)換為 Set 再完成的。
Collection<User> users = princialCollection.fromRealm("c");
根據(jù) Realm 名字獲取身份,因?yàn)?Realm 名字可以重復(fù),所以可能多個(gè)身份,建議 Realm 名字盡量不要重復(fù)。
AuthorizationInfo 用于聚合授權(quán)信息的:
public interface AuthorizationInfo extends Serializable {
Collection<String> getRoles(); //獲取角色字符串信息
Collection<String> getStringPermissions(); //獲取權(quán)限字符串信息
Collection<Permission> getObjectPermissions(); //獲取Permission對(duì)象信息
}
當(dāng)我們使用 AuthorizingRealm 時(shí),如果身份驗(yàn)證成功,在進(jìn)行授權(quán)時(shí)就通過(guò) doGetAuthorizationInfo 方法獲取角色 / 權(quán)限信息用于授權(quán)驗(yàn)證。
Shiro 提供了一個(gè)實(shí)現(xiàn) SimpleAuthorizationInfo,大多數(shù)時(shí)候使用這個(gè)即可。
對(duì)于 Account 及 SimpleAccount,之前的【6.3 AuthenticationInfo】已經(jīng)介紹過(guò)了,用于 SimpleAccountRealm 子類,實(shí)現(xiàn)動(dòng)態(tài)角色 / 權(quán)限維護(hù)的。
Subject 是 Shiro 的核心對(duì)象,基本所有身份驗(yàn)證、授權(quán)都是通過(guò) Subject 完成。
1、身份信息獲取
Object getPrincipal(); //Primary Principal
PrincipalCollection getPrincipals(); // PrincipalCollection
2、身份驗(yàn)證
void login(AuthenticationToken token) throws AuthenticationException;
boolean isAuthenticated();
boolean isRemembered();
通過(guò) login 登錄,如果登錄失敗將拋出相應(yīng)的 AuthenticationException,如果登錄成功調(diào)用 isAuthenticated 就會(huì)返回 true,即已經(jīng)通過(guò)身份驗(yàn)證;如果 isRemembered 返回 true,表示是通過(guò)記住我功能登錄的而不是調(diào)用 login 方法登錄的。isAuthenticated/isRemembered 是互斥的,即如果其中一個(gè)返回 true,另一個(gè)返回 false。
3、角色授權(quán)驗(yàn)證
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 進(jìn)行角色驗(yàn)證,驗(yàn)證后返回 true/false;而 checkRole 驗(yàn)證失敗時(shí)拋出 AuthorizationException 異常。
4、權(quán)限授權(quán)驗(yàn)證
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 進(jìn)行權(quán)限驗(yàn)證,驗(yàn)證后返回 true/false;而 checkPermission 驗(yàn)證失敗時(shí)拋出 AuthorizationException。
5、會(huì)話
Session getSession(); //相當(dāng)于getSession(true)
Session getSession(boolean create);
類似于 Web 中的會(huì)話。如果登錄成功就相當(dāng)于建立了會(huì)話,接著可以使用 getSession 獲取;如果 create=false 如果沒(méi)有會(huì)話將返回 null,而 create=true 如果沒(méi)有會(huì)話會(huì)強(qiáng)制創(chuàng)建一個(gè)。
6、退出
void logout();
7、RunAs
void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException;
boolean isRunAs();
PrincipalCollection getPreviousPrincipals();
PrincipalCollection releaseRunAs();
RunAs 即實(shí)現(xiàn) “允許 A 假設(shè)為 B 身份進(jìn)行訪問(wèn)”;通過(guò)調(diào)用 subject.runAs(b) 進(jìn)行訪問(wèn);接著調(diào)用 subject.getPrincipals 將獲取到 B 的身份;此時(shí)調(diào)用 isRunAs 將返回 true;而 a 的身份需要通過(guò) subject. getPreviousPrincipals 獲取;如果不需要 RunAs 了調(diào)用 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);
實(shí)現(xiàn)線程之間的 Subject 傳播,因?yàn)?Subject 是線程綁定的;因此在多線程執(zhí)行中需要傳播到相應(yīng)的線程才能獲取到相應(yīng)的 Subject。最簡(jiǎn)單的辦法就是通過(guò) execute(runnable/callable 實(shí)例) 直接調(diào)用;或者通過(guò) associateWith(runnable/callable 實(shí)例) 得到一個(gè)包裝后的實(shí)例;它們都是通過(guò):1、把當(dāng)前線程的 Subject 綁定過(guò)去;2、在線程執(zhí)行結(jié)束后自動(dòng)釋放。
Subject 自己不會(huì)實(shí)現(xiàn)相應(yīng)的身份驗(yàn)證 / 授權(quán)邏輯,而是通過(guò) DelegatingSubject 委托給 SecurityManager 實(shí)現(xiàn);及可以理解為 Subject 是一個(gè)面門。
對(duì)于 Subject 的構(gòu)建一般沒(méi)必要我們?nèi)?chuàng)建;一般通過(guò) SecurityUtils.getSubject() 獲?。?
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
即首先查看當(dāng)前線程是否綁定了 Subject,如果沒(méi)有通過(guò) Subject.Builder 構(gòu)建一個(gè)然后綁定到現(xiàn)場(chǎng)返回。
如果想自定義創(chuàng)建,可以通過(guò):
new Subject.Builder().principals(身份).authenticated(true/false).buildSubject()
這種可以創(chuàng)建相應(yīng)的 Subject 實(shí)例了,然后自己綁定到線程即可。在 new Builder() 時(shí)如果沒(méi)有傳入 SecurityManager,自動(dòng)調(diào)用 SecurityUtils.getSecurityManager 獲?。灰部梢宰约簜魅胍粋€(gè)實(shí)例。
對(duì)于 Subject 我們一般這么使用:
而我們必須的功能就是 1、2、5。到目前為止我們就可以使用 Shiro 進(jìn)行應(yīng)用程序的安全控制了,但是還是缺少如對(duì) Web 驗(yàn)證、Java 方法驗(yàn)證等的一些簡(jiǎn)化實(shí)現(xiàn)。
更多建議: