Shrio原始碼分析(4) - 資料域(Realm)
本文在於分析Shiro原始碼,對於新學習的朋友可以參考
[開濤部落格](http://jinnianshilongnian.iteye.com/blog/2018398)進行學習。
本篇主要分析Shiro中的Realm介面。Shiro使用Realm介面作為外部資料來源,主要處理認證和授權工作。Realm介面如下。
public interface Realm {
/**
* Realm必須要有一個唯一的名稱
*/
String getName();
/**
* 判斷該Realm是否支援處理給定的token認證
*/
boolean supports(AuthenticationToken token);
/**
* 認證token,並返回已認證的AuthenticationInfo
* 如果沒有賬戶可以認證,返回null,如果認證失敗丟擲異常
*/
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}
CachingRealm抽象類
CachingRealm是帶有快取功能的Realm抽象實現。在CachingRealm中提供了對Realm進行快取功能的快取管理器CacheManager,但並沒有實現具體快取什麼。在CachingRealm中提供了對onLogout的處理,該方法從LogoutAware實現來,用來處理使用者登出後清理快取資料。Shiro預設對Realm開啟快取功能。
// Realm名稱
private String name;
// 是否開啟快取,預設構造方法開啟快取
private boolean cachingEnabled;
// 快取管理器
private CacheManager cacheManager;
public CachingRealm() {
this.cachingEnabled = true;
this.name = getClass().getName() + "_" + INSTANCE_COUNT.getAndIncrement();
}
值得一提的是afterCacheManagerSet()這個鉤子方法,在設定快取處理器後會呼叫這個方法,在後面的分析中會由子類重寫。
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
afterCacheManagerSet();
}
protected void afterCacheManagerSet() {
}
AuthenticatingRealm抽象類
AuthenticatingRealm是一個可認證的Realm抽象實現類。 AuthenticatingRealm繼承了CachingRealm,並實現了Initializable。Initializable提供的init()方法在初始化時會呼叫。下面是AuthenticatingRealm的屬性和構造方法。
// 憑證匹配器,用來匹配憑證是否正確
private CredentialsMatcher credentialsMatcher;
// 快取通過認證的認證資料
private Cache<Object, AuthenticationInfo> authenticationCache;
// 是否認證快取
private boolean authenticationCachingEnabled;
// 認證快取的名稱
private String authenticationCacheName;
/**
* 定義Realm支援的AuthenticationToken型別
*/
private Class<? extends AuthenticationToken> authenticationTokenClass;
public AuthenticatingRealm() {
this(null, new SimpleCredentialsMatcher());
}
public AuthenticatingRealm(CacheManager cacheManager) {
this(cacheManager, new SimpleCredentialsMatcher());
}
public AuthenticatingRealm(CredentialsMatcher matcher) {
this(null, matcher);
}
public AuthenticatingRealm(CacheManager cacheManager, CredentialsMatcher matcher) {
// 預設支援UsernamePasswordToken型別
authenticationTokenClass = UsernamePasswordToken.class;
// 認證不快取
this.authenticationCachingEnabled = false;
// 設定認證快取的名稱
int instanceNumber = INSTANCE_COUNT.getAndIncrement();
this.authenticationCacheName = getClass().getName() + DEFAULT_AUTHORIZATION_CACHE_SUFFIX;
if (instanceNumber > 0) {
this.authenticationCacheName = this.authenticationCacheName + "." + instanceNumber;
}
// 設定快取管理器
if (cacheManager != null) {
setCacheManager(cacheManager);
}
// 設定憑證匹配器
if (matcher != null) {
setCredentialsMatcher(matcher);
}
}
從屬性和構造方法我們可以看出,AuthenticatingRealm會進行認證,對認證的結果AuthenticationInfo進行快取,認證時需要使用憑證匹配器來匹配憑證是否正確。下面,我們根據這個思路可以去看看進行認證的方法getAuthenticationInfo(AuthenticationToken token)。
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 從認證快取中獲取認證結果
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
// 這是一個抽象方法,子類去完成認證過程
info = doGetAuthenticationInfo(token);
if (token != null && info != null) {
// 如果認證通過,則將認證結果快取起來
cacheAuthenticationInfoIfPossible(token, info);
}
}
// 匹配憑證是否正確,如果不正確將會丟擲異常
if (info != null) {
assertCredentialsMatch(token, info);
}
return info;
}
關於AuthenticationInfo快取過程中的一些細節。在快取的過程中是以AuthenticationToken中的身份進行快取的,所有身份肯定要是唯一的。屬性authenticationCache可以由外部提供,也可以通過快取管理器生成,一般情況下authenticationCache不需要外部設定。
AuthorizingRealm抽象類
AuthorizingRealm繼承了AuthenticatingRealm,負責處理角色和許可權。AuthorizingRealm的實現方式和AuthenticatingRealm一樣,提供了一個抽象的doGetAuthorizationInfo(PrincipalCollection principals)方法。這裡不做詳細介紹,我們會在後面分析角色許可權時介紹。
基於Jdbc的Realm(JdbcRealm類)
JdbcRealm類可以直接和資料庫連線,從資料中獲取使用者名稱、密碼、角色、許可權等資料資訊。通過和資料庫的直接連線來判斷認證是否正確,是否有角色許可權功能。
在JdbcRealm中提供了一些Sql語句常量,通過這些sql來做資料庫操作。當然,操作資料肯定需要資料庫資料來源。
// 通過使用者名稱查詢密碼的Sql語句
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
/**
* 通過使用者名稱稱查詢密碼和加密鹽的Sql語句
*/
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
/**
* 通過使用者名稱查詢使用者所有角色
*/
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
/**
* 通過角色名稱查詢角色擁有的許可權
*/
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
/**
* 定義了幾種加鹽模式:
* NO_SALT - 密碼沒有加密鹽
* CRYPT - unix加密(這種模式目前還支援)
* COLUMN - 加密鹽儲存在資料庫表字段中
* EXTERNAL - 加密鹽沒有儲存在資料庫
*/
public enum SaltStyle {NO_SALT, CRYPT, COLUMN, EXTERNAL};
// 資料庫資料來源
protected DataSource dataSource;
// 查詢密碼和加密鹽的SQL
protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;
// 查詢使用者角色的SQL
protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;
// 查詢角色擁有許可權的SQL
protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;
protected boolean permissionsLookupEnabled = false;
// 密碼沒有加密鹽模式
protected SaltStyle saltStyle = SaltStyle.NO_SALT;
對於不同的資料庫,這些預設的Sql是可以更改的,JdbcRealm都提供了相應的setter方法。那麼,Jdbc是如何認證和獲取角色許可權的呢?下面繼續分析doGetAuthenticationInfo和doGetAuthorizationInfo這兩個方法。
- 認證過程
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 只支援UsernamePasswordToken型別
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
// 使用者名稱
String username = upToken.getUsername();
// 使用者名稱空判斷
if (username == null) {
throw new AccountException("Null usernames are not allowed by this realm.");
}
Connection conn = null;
SimpleAuthenticationInfo info = null;
try {
// 獲取資料庫連線
conn = dataSource.getConnection();
String password = null;
String salt = null;
switch (saltStyle) {
case NO_SALT:
password = getPasswordForUser(conn, username)[0];
break;
case CRYPT:
// TODO: separate password and hash from getPasswordForUser[0]
throw new ConfigurationException("Not implemented yet");
//break;
case COLUMN:
String[] queryResults = getPasswordForUser(conn, username);
password = queryResults[0];
salt = queryResults[1];
break;
case EXTERNAL:
password = getPasswordForUser(conn, username)[0];
// 以使用者名稱作為加密鹽
salt = getSaltForUser(username);
}
if (password == null) {
throw new UnknownAccountException("No account found for user [" + username + "]");
}
// 建立一個認證資訊
info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
if (salt != null) {
info.setCredentialsSalt(ByteSource.Util.bytes(salt));
}
} catch (SQLException e) {
throw new AuthenticationException(message, e);
} finally {
// 關閉資料連線
JdbcUtils.closeConnection(conn);
}
return info;
}
2 授權過程
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 身份不能為空
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
// 從身份中獲取使用者名稱
String username = (String) getAvailablePrincipal(principals);
Connection conn = null;
Set<String> roleNames = null;
Set<String> permissions = null;
try {
// 獲取資料庫連線
conn = dataSource.getConnection();
// 獲取角色集合
roleNames = getRoleNamesForUser(conn, username);
if (permissionsLookupEnabled) {
// 獲取許可權集合
permissions = getPermissions(conn, username, roleNames);
}
} catch (SQLException e) {
throw new AuthorizationException(message, e);
} finally {
JdbcUtils.closeConnection(conn);
}
// 返回帶有角色許可權的認證資訊
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
info.setStringPermissions(permissions);
return info;
}
在Shiro中還提供了一些其他的Realm。SimpleAccountRealm、TextConfigurationRealm、IniRealm、PropertiesRealm。這裡就不一一介紹了,有興趣可以自己去看。
總結
在Shiro中Realm介面作為一個與應用程式外接的介面,可以通過Realm提供認證和授權的資料資訊。在開發使用中最常用的就是從AuthenticatingRealm或AuthorizingRealm抽象類來實現業務中具體的Realm例項。doGetAuthenticationInfo(AuthenticationToken token)處理認證過程,doGetAuthorizationInfo(PrincipalCollection principals)處理授權過程。