## 6.1 Realm
【Realm】及【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)**
``` 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 中沒有進行身份及憑據驗證,所以相當于身份驗證都是成功的,都將返回:
``` 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.5 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.6 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 方法驗證等的一些簡化實現。
- 第1章 Shiro簡介
- 1.1 簡介
- 第2章 身份驗證
- 第3章 授權
- 第4章 INI配置
- 第5章 編碼 / 加密
- 第6章 Realm及相關對象
- 第7章 與Web集成
- 第8章 攔截器機制
- 第9章 JSP標簽
- 第10章 會話管理
- 第11章 緩存機制
- 第12章 與Spring集成
- 第13章 RememberMe
- 第14章 SSL
- 第15章 單點登錄
- 第16章 綜合實例
- 第17章 OAuth2集成
- 第18章 并發登錄人數控制
- 第19章 動態URL權限控制
- 第20章 無狀態Web應用集成
- 第21章 授予身份及切換身份
- 第22章 集成驗證碼
- 第23章 多項目集中權限管理及分布式會話
- 第24章 在線會話管理