1. 程式人生 > >Shrio原始碼分析(4) - 資料域(Realm)

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這兩個方法。

  1. 認證過程
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)處理授權過程。