Shiro - 關於Realm

以前在Authentication和Authorization中也提到Realm。緩存

不管是身份驗證仍是權限驗證,不管數據以什麼方式存在,咱們都須要訪問一些數據並將其轉換爲Shiro能夠識別的格式。安全

一般一個數據源對應一個Realm。所以,實現一個Realm時會用到該數據源相關的API。框架

一般一個數據源中會同時保存身份相關數據與權限相關數據。所以,一個Realm實現類能夠進行認證和受權兩種操做。eclipse

 

能夠將Realm簡單地理解爲DAO。this

(雖然IDEA生成的type hirarchy diagram很漂亮,可是太大了...仍是用回eclipse截圖..)debug

若是使用.ini配置,咱們能夠在[main]部分定義N個Realm,但咱們能夠用顯式(explicit)和隱式(implicit)兩種方式爲securityManager指定Realm(有點IOC容器的意思)。
code

顯示指定就是常見的方式,即定義Realm後再爲securityManager按須要的順序指定Realm。orm

fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm

securityManager.realms = $fooRealm, $barRealm, $bazRealm


顯示指定的方式相對較爲明確,即便不改變Realm的定義,咱們仍可讓驗證和受權按咱們給securityManager指定的順序的Realm來執行。
blog

若是由於某些緣由(多是定義的Realm太多?)不想爲securityManager.realms指定,咱們也可使用隱式方式。
繼承

也就是說,把上面的配置改爲以下形式就是隱式方式了:

blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm

 

隱式方式其實就是不指定,只定義(define),Shiro會搜索配置中全部的Realm並將它們一一指定給securityManager。

使用隱式方式時只要稍微改一下Realm的定義,Shiro就可能會給咱們來個驚喜。

 

在介紹Authentication的文章中,說的是當一個驗證請求出現時Shiro框架的工做流程。

在這裏具體記錄一下Realm負責的工做(雖說securiyManager驗證開始的地方,但從數據源取數據並做比較的工做是由Realm來進行的)。

以ModularRealmAuthenticator爲例:

protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {

        AuthenticationStrategy strategy = getAuthenticationStrategy();

        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);

        if (log.isTraceEnabled()) {
            log.trace("Iterating through {} realms for PAM authentication", realms.size());
        }

        for (Realm realm : realms) {

            aggregate = strategy.beforeAttempt(realm, token, aggregate);

            if (realm.supports(token)) {

                log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);

                AuthenticationInfo info = null;
                Throwable t = null;
                try {
                    info = realm.getAuthenticationInfo(token);
                } catch (Throwable throwable) {
                    t = throwable;
                    if (log.isDebugEnabled()) {
                        String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
                        log.debug(msg, t);
                    }
                }

                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);

            } else {
                log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
            }
        }

        aggregate = strategy.afterAllAttempts(token, aggregate);

        return aggregate;
    }

 

在Realm開始處理驗證的邏輯以前,Authenticator將調用Realm的supports方法去驗證當前Realm是否支持得到的AuthenticationToken。

一般,Realm檢查的是token的類型,好比在AuthenticatingRealm中檢查類型是否相同。

public boolean supports(AuthenticationToken token) {
    return token != null && getAuthenticationTokenClass().isAssignableFrom(token.getClass());
}

 

另外,AuthenticatingRealm的constructor中類型默認爲

authenticationTokenClass = UsernamePasswordToken.class;


若是當前Realm支持提交過來的token,authenticator則調用getAuthenticationInfo(token)方法。

以AuthenticatingRealm爲例(注意是final):

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    AuthenticationInfo info = getCachedAuthenticationInfo(token);
    if (info == null) {
        //otherwise not cached, perform the lookup:
        info = doGetAuthenticationInfo(token);
        log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
        if (token != null && info != null) {
            cacheAuthenticationInfoIfPossible(token, info);
        }
    } else {
        log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
    }

    if (info != null) {
        assertCredentialsMatch(token, info);
    } else {
        log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
    }

    return info;
}

 

若是能夠從緩存中得到驗證信息,下一步則檢查密碼是否匹配,即assertCredentialsMatch(token, info)。

若是緩存中不存在驗證信息則調用如下方法。

protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;

 

這裏咱們暫時先不考慮緩存的狀況,考慮doGetAuthenticationInfo應該作什麼。

有些人(好比我)直接在該方法中完成也驗證,驗證經過時返回SimpleAuthenticationInfo實例,失敗則拋出相應的驗證異常。

但下面有個assertCredentialsMatch,說明doGetAuthenticationInfo本沒有打算這樣用,這種使用方式會讓CredentialMatcher失去意義。

參考JdbcRealm的實現,只是根據身份(用戶名)去查詢並返回SimpleAuthenticationInfo實例。

而後讓assertCredentialsMatch比較token和authenticationInfo。

protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
    CredentialsMatcher cm = getCredentialsMatcher();
    if (cm != null) {
        if (!cm.doCredentialsMatch(token, info)) {
            //not successful - throw an exception to indicate this:
            String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
            throw new IncorrectCredentialsException(msg);
        }
    } else {
        throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
                "credentials during authentication.  If you do not wish for credentials to be examined, you " +
                "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
    }
}

 

既然提到了CredentialMatcher,咱們來看看他的意義所在。 首先說明,AuthenticatingRealm的默認CredentialMatcher是...

public AuthenticatingRealm() {
    this(null, new SimpleCredentialsMatcher());
}


若是僅僅是作密碼字符比較咱們大可沒必要作出這樣一個接口(字符串比較的可插拔+可定製麼?)

以前在說Authentication的時候就提過,AuthenticationToken的Principal和Credential能夠是任何類型的,光是拿過來直接比較是否相符也不僅是比較密碼字符那麼簡單了。

SimpleCredentialsMatcher就是用來比較兩個credential是否相同的。

其doCredentialsMatch方法返回其equals方法的返回值。

protected boolean equals(Object tokenCredentials, Object accountCredentials) {
    if (log.isDebugEnabled()) {
        log.debug("Performing credentials equality check for tokenCredentials of type [" +
                tokenCredentials.getClass().getName() + " and accountCredentials of type [" +
                accountCredentials.getClass().getName() + "]");
    }
    if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) {
        if (log.isDebugEnabled()) {
            log.debug("Both credentials arguments can be easily converted to byte arrays.  Performing " +
                    "array equals comparison");
        }
        byte[] tokenBytes = toBytes(tokenCredentials);
        byte[] accountBytes = toBytes(accountCredentials);
        return Arrays.equals(tokenBytes, accountBytes);
    } else {
        return accountCredentials.equals(tokenCredentials);
    }
}

 

固然,實現類不僅是SimpleCredentialsMatcher...

 

SimpleCredentialsMatcher下還跟着HashedCredentialsMatcher,再往下就都deprecated了。

說到HashedCredentialsMatcher,他只是給密碼加個salt以提升安全。

其中hashSalted屬性基本不用考慮,由於從Shiro 1.1開始salt是根據SaltedAuthenticationInfo的getCredentialSalt()方法返回的non-null value。

public HashedCredentialsMatcher() {
    this.hashAlgorithm = null;
    this.hashSalted = false;
    this.hashIterations = 1;
    this.storedCredentialsHexEncoded = true; //false means Base64-encoded
}

 

說到salting就不得不說SaltedAuthenticationInfo,該接口繼承AuthenticationInfo,即除了principal和credential,他還有一個credentialsSalt。

public interface SaltedAuthenticationInfo extends AuthenticationInfo {

    /**
     * Returns the salt used to salt the account's credentials or {@code null} if no salt was used.
     *
     * @return the salt used to salt the account's credentials or {@code null} if no salt was used.
     */
    ByteSource getCredentialsSalt();
}

 

HashedCredentialsMatcher有個默認的salt,是將本身的principal做爲salt,後來這個方法也被deprecated了。

由於從1.1開始,Shiro禁止salt從用戶的登陸信息中獲取,而應該從數據源獲取。

@Deprecated
protected Object getSalt(AuthenticationToken token) {
    return token.getPrincipal();
}

這一系列方法和屬性會從Shiro 2.0開始完全消失。

相關文章
相關標籤/搜索