第六章 Realm及相關物件(一) Realm
一、Realm
1. 定義實體及關係
即使用者-角色之間是多對多關係,角色-許可權之間是多對多關係;且使用者和許可權之間通過角色建立關係;在系統中驗證時通過許可權驗證,角色只是許可權集合,即所謂的顯示角色;其實許可權應該對應到資源(如選單、URL、頁面按鈕、Java 方法等)中,即應該將許可權字串儲存到資源實體中,但是目前為了簡單化,直接提取一個許可權表,【綜合示例】部分會使用完整的表結構。
使用者實體包括:編號(id)、使用者名稱(username)、密碼(password)、鹽(salt)、是否鎖定(locked);是否鎖定用於封禁使用者使用,其實最好使用Enum 欄位儲存,可以實現更復雜的使用者狀態實現。
角色實體包括:、編號(id)、角色識別符號(role)、描述(description)、是否可用(available);其中角色識別符號用於在程式中進行隱式角色判斷的,描述用於以後再前臺介面顯示的、是否可用表示角色當前是否啟用。
許可權實體包括:編號(id)、許可權識別符號(permission)、描述(description)、是否可用(available);含義和角色實體類似不再闡述。
另外還有兩個關係實體:使用者-角色實體(使用者編號、角色編號,且組合為複合主鍵);角色-許可權實體(角色編號、許可權編號,且組合為複合主鍵)。
2. 環境準備
為了方便資料庫操作,使用了“org.springframework: spring-jdbc: 4.0.0.RELEASE”依賴,雖然是spring4版本的,但使用上和spring3 無區別。其他依賴請參考原始碼的pom.xml。
3. 定義Service及Dao
為了實的簡單性,只實現必須的功能,其他的可以自己實現即可。
public interface PermissionService {
public Permission createPermission(Permission permission);
public void deletePermission(Long permissionId);
}
實現基本的建立/刪除許可權。
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 多了關聯/移除關聯角色-許可權功能。
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就使用這些方法來查詢相關資訊。
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。
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
和第五章一樣。
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher{
private Ehcache passwordRetryCache;
public RetryLimitHashedCredentialsMatcher() {
CacheManager cacheManager = CacheManager.newInstance(CacheManager.class.getClassLoader().getResource("chapter6/ini/ehcache.xml"));
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
public boolean doCredentialsMatch(AuthenticationToken token,AuthenticationInfo info) {
String username = (String) token.getPrincipal();
Element element = passwordRetryCache.get(username);
if(element == null){
element = new Element(username,new AtomicInteger(0));
passwordRetryCache.put(element);
}
AtomicInteger retryCount = (AtomicInteger) element.getObjectValue();
if(retryCount.incrementAndGet()>5){
//if retry count > 5 throw
throw new ExcessiveAttemptsException();
}
boolean matches = super.doCredentialsMatch(token, info);
if(matches){
//clear retry count
passwordRetryCache.remove(username);
}
return matches;
}
}
UserRealm
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。包含了:登入成功、使用者名稱錯誤、密碼錯誤、密碼超出重試次數、有/沒有角色、有/沒有許可權的測試。
完整例子見我的資源庫下載: