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

第六章 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。包含了:登入成功、使用者名稱錯誤、密碼錯誤、密碼超出重試次數、有/沒有角色、有/沒有許可權的測試。

完整例子見我的資源庫下載: