1. 程式人生 > >Shiro原始碼分析(3) - 認證器(Authenticator)

Shiro原始碼分析(3) - 認證器(Authenticator)

本文在於分析Shiro原始碼,對於新學習的朋友可以參考
[開濤部落格](http://jinnianshilongnian.iteye.com/blog/2018398)進行學習。

Authenticator就是認證器,在Shiro中負責認證使用者提交的資訊,在Shiro中我們用AuthenticationToken來表示提交的資訊。Authenticator介面只提供了一個認證的方法。如下。

/**
 * 認證使用者提交的資訊AuthenticationToken物件,AuthenticationToken包含了身份和憑證。
 * 如果認證成功則返回AuthenticationInfo物件,AuthenticationInfo物件代表了使用者在Shiro中已經被認證過的賬戶資料。
 * 如果認證失敗則丟擲一下異常
 * @see
ExpiredCredentialsException 憑證過期 * @see IncorrectCredentialsException 憑證錯誤 * @see ExcessiveAttemptsException 多次嘗試失敗 * @see LockedAccountException 賬戶鎖定 * @see ConcurrentAccessException 併發訪問異常(多點登入) * @see UnknownAccountException 賬戶未知 */
public AuthenticationInfo authenticate
(AuthenticationToken authenticationToken) throws AuthenticationException
;

AuthenticationToken分析

AuthenticationToken物件代表了身份和憑證。從下面的介面看提供的方法很簡單,但返回的物件都是Object,也就是說在Shiro中對身份和憑證的型別沒有限制,Shiro沒有提供特有的型別來處理。

public interface AuthenticationToken extends Serializable {
    /**
     *  獲取身份
     */
Object getPrincipal(); /** * 獲取憑證 */ Object getCredentials(); }

在Shiro中只提供了一種具體的實現類UsernamePasswordToken。UsernamePasswordToken類是以使用者名稱作為身份,密碼作為憑證。當然它也實現了RememberMeAuthenticationToken介面,提供rememberMe功能。rememberMe功能的實現在後面再分析,在這裡不是重點。UsernamePasswordToken很簡單,只有構造方法和setter/getter方法。我們需要對UsernamePasswordToken中的身份和憑證要有很好的理解,什麼可以作為身份,什麼又是憑證。

AuthenticationInfo分析

AuthenticationInfo表示被Subject儲存的賬戶,這個賬戶是經過認證的。而AuthenticationToken中的身份/憑證是使用者提交的資料,還沒有經過認證,如果認證成功才會被儲存在AuthenticationInfo中。

AuthenticationInfo只有一個實現類SimpleAuthenticationInfo(備註:SimpleAccount也是其中一個實現類,但功能是完全依賴SimpleAuthenticationInfo實現的)。AuthenticationInfo還有兩個子介面,分別是:SaltedAuthenticationInfo和MergableAuthenticationInfo。SaltedAuthenticationInfo提供了獲取憑證加密鹽的方法,MergableAuthenticationInfo可以合併驗證後的身份資訊。各自的介面都很簡單,下面直接分析SimpleAuthenticationInfo具體實現。

SimpleAuthenticationInfo詳細分析

下面是SimpleAuthenticationInfo的屬性和構造方法。下面有很多構造方法,但可以看出實現都依賴到SimplePrincipalCollection物件。SimplePrincipalCollection物件負責收集身份(principals)和域(realm)的關係。關於SimplePrincipalCollection我們暫時不展開分析。

/**
 * 身份集合
 */
protected PrincipalCollection principals;
/**
 * 憑證
 */
protected Object credentials;
/**
 * 憑證鹽
 */
protected ByteSource credentialsSalt;
public SimpleAuthenticationInfo() {
}
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
    this.principals = new SimplePrincipalCollection(principal, realmName);
    this.credentials = credentials;
}
public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {
    this.principals = new SimplePrincipalCollection(principal, realmName);
    this.credentials = hashedCredentials;
    this.credentialsSalt = credentialsSalt;
}
public SimpleAuthenticationInfo(PrincipalCollection principals, Object credentials) {
    this.principals = new SimplePrincipalCollection(principals);
    this.credentials = credentials;
}
public SimpleAuthenticationInfo(PrincipalCollection principals, Object hashedCredentials, ByteSource credentialsSalt) {
    this.principals = new SimplePrincipalCollection(principals);
    this.credentials = hashedCredentials;
    this.credentialsSalt = credentialsSalt;
}

在SimpleAuthenticationInfo中,我們主要分析一下merge(AuthenticationInfo info)方法,也就是說可以合併其他的AuthenticationInfo資訊。

public void merge(AuthenticationInfo info) {
    // 判斷是否有身份資訊,如果沒有就返回
    if (info == null || info.getPrincipals() == null || info.getPrincipals().isEmpty()) {
        return;
    }
    // 合併身份集合
    if (this.principals == null) {
        this.principals = info.getPrincipals();
    } else {
        if (!(this.principals instanceof MutablePrincipalCollection)) {
            this.principals = new SimplePrincipalCollection(this.principals);
        }
        ((MutablePrincipalCollection) this.principals).addAll(info.getPrincipals());
    }
    // 憑證鹽只是在Realm憑證匹配過程中使用
    // 如果存在憑證鹽,就不用管其他的了,如果沒有就使用其他的憑證鹽
    if (this.credentialsSalt == null && info instanceof SaltedAuthenticationInfo) {
        this.credentialsSalt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
    }
    // 合併憑證資訊
    Object thisCredentials = getCredentials();
    Object otherCredentials = info.getCredentials();
    if (otherCredentials == null) {
        return;
    }
    if (thisCredentials == null) {
        this.credentials = otherCredentials;
        return;
    }
    // 使用集合來合併憑證
    if (!(thisCredentials instanceof Collection)) {
        Set newSet = new HashSet();
        newSet.add(thisCredentials);
        setCredentials(newSet);
    }
    Collection credentialCollection = (Collection) getCredentials();
    if (otherCredentials instanceof Collection) {
        credentialCollection.addAll((Collection) otherCredentials);
    } else {
        credentialCollection.add(otherCredentials);
    }
}

AbstractAuthenticator抽象類

和AbstractSessionManager一樣,AbstractAuthenticator主要功能也是提供監聽器,對認證過程中的狀態進行監聽。在認證過程中監聽成功、失敗、登出情況。監聽器是AuthenticationListener,下面看一下監聽器提供的方法。

public interface AuthenticationListener {
    /**
     * 監聽認證成功
     */
    void onSuccess(AuthenticationToken token, AuthenticationInfo info);
    /**
     * 監聽認證失敗
     */
    void onFailure(AuthenticationToken token, AuthenticationException ae);
    /**
     * 監聽使用者登出
     */
    void onLogout(PrincipalCollection principals);
}

另外,AbstractAuthenticator實現了Authenticator#authenticate(AuthenticationToken token),處理了對監聽器通知的情況,但執行認證的具體過程提供抽象方法doAuthenticate(AuthenticationToken token)讓子類完成。

public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    // Token引數異常
    if (token == null) {
        throw new IllegalArgumentException("Method argumet (authentication token) cannot be null.");
    }
    AuthenticationInfo info;
    try {
        // 執行認證過程的抽象方法,子類去實現
        info = doAuthenticate(token);
        if (info == null) {
            String msg = "No account information found for authentication token [" + token + "] by this " +
                    "Authenticator instance.  Please check that it is configured correctly.";
            throw new AuthenticationException(msg);
        }
    } catch (Throwable t) {
        AuthenticationException ae = null;
        if (t instanceof AuthenticationException) {
            ae = (AuthenticationException) t;
        }
        if (ae == null) {
            String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected " +
                    "error? (Typical or expected login exceptions should extend from AuthenticationException).";
            ae = new AuthenticationException(msg, t);
        }
        try {
            // 認證失敗了,通知監聽器
            notifyFailure(token, ae);
        } catch (Throwable t2) {
        }
        throw ae;
    }
    // 認證成功,通知監聽器
    notifySuccess(token, info);
    return info;
}

ModularRealmAuthenticator類分析

在Shiro中只提供了一個具體的實現類ModularRealmAuthenticator,該類可以處理多個Realm的認證方式。

在ModularRealmAuthenticator中,把認證的權利交給域(Realm)去完成,在Shiro中Realm相當於資料的來源,可以自定義。ModularRealmAuthenticator支援多個Realm進行認證,在多個Realm認證時,需要設定認證策略,策略介面是AuthenticationStrategy。在Shiro中提供了三種認證策略。分別是:

  1. AllSuccessfulStrategy:所有Realm認證成功。
  2. AtLeastOneSuccessfulStrategy:至少有一個Realm認證成功。
  3. FirstSuccessfulStrategy: 第一個Realm認證成功。

關於認證策略我們在後面在分析,現在繼續分析ModularRealmAuthenticator。我們還是從屬性和構造方法分析。

// 認證的過程交由Realm去處理
private Collection<Realm> realms;
// 指定認證策略
private AuthenticationStrategy authenticationStrategy;
public ModularRealmAuthenticator() {
    // 預設提供策略:至少有一個Realm認證成功就算認證成功
    this.authenticationStrategy = new AtLeastOneSuccessfulStrategy();
}

下面我看看ModularRealmAuthenticator是如何實現doAuthenticate(token)方法的。

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    // 判斷realms屬性,必須要有Realm
    assertRealmsConfigured();
    Collection<Realm> realms = getRealms();
    // 分支:如果只有一個就按照單個流程處理,如果有多個Realm就按照多個流程走認證策略。
    if (realms.size() == 1) {
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}
// 處理單個Realm
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    // 判斷realm是否支援處理token
    if (!realm.supports(token)) {
        String msg = "Realm [" + realm + "] does not support authentication token [" +
                token + "].  Please ensure that the appropriate Realm implementation is " +
                "configured correctly or that the realm accepts AuthenticationTokens of this type.";
        throw new UnsupportedTokenException(msg);
    }
    // realm處理認證過程,處理過程中可能會丟擲認證異常AuthenticationException。
    // 如果認證成功info不會用null。
    AuthenticationInfo info = realm.getAuthenticationInfo(token);
    if (info == null) {
        String msg = "Realm [" + realm + "] was unable to find account data for the " +
                "submitted AuthenticationToken [" + token + "].";
        throw new UnknownAccountException(msg);
    }
    return info;
}
// 處理多個Realm
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
    AuthenticationStrategy strategy = getAuthenticationStrategy();
    // 返回一個空的聚合物件
    // AllSuccessfulStrategy - 返回空的SimpleAuthenticationInfo物件
    // AtLeastOneSuccessfulStrategy - 返回空的SimpleAuthenticationInfo物件
    // FirstSuccessfulStrategy - 返回null
    AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
    for (Realm realm : realms) {
        // 認證前處理
        // AllSuccessfulStrategy - 判斷realm.supports(token),如果不支援直接拋異常,返回aggregate
        // AtLeastOneSuccessfulStrategy - 返回aggregate
        // FirstSuccessfulStrategy - 返回aggregate,也就是null
        aggregate = strategy.beforeAttempt(realm, token, aggregate);
        if (realm.supports(token)) {
            AuthenticationInfo info = null;
            Throwable t = null;
            try {
                //認證過程是由Realm處理的
                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);
                }
            }
            // 認證後處理,
            // AllSuccessfulStrategy - 如果有異常會丟擲異常, 如果沒有就合併info和aggregate
            // AtLeastOneSuccessfulStrategy - 如果有異常並不會丟擲,只是會合併info和aggregate
            // FirstSuccessfulStrategy - 如果aggregate存在,則返回aggregate;否則返回info
            aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
        } else {
            log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
        }
    }
    // AllSuccessfulStrategy - 返回aggregate
    // AtLeastOneSuccessfulStrategy - 判斷aggregate不為空,否則拋異常
    // FirstSuccessfulStrategy - 返回aggregate
    aggregate = strategy.afterAllAttempts(token, aggregate);
    return aggregate;
}    

通過對上面的程式碼分析:

  • AllSuccessfulStrategy策略流程:逐一處理Realm,每個Realm必須支援token處理,然後合併AuthenticationInfo。如果遇到異常,則丟擲異常結束迴圈。
  • AtLeastOneSuccessfulStrategy策略流程:逐一處理Realm,不支援處理token的Realm跳過。如果遇到異常,忽略對異常的處理。對於認證通過的AuthenticationInfo進行合併成aggregate,最後判斷aggregate,aggregate不能為空,如果有空丟擲異常。
  • FirstSuccessfulStrategy策略流程:逐一處理Realm,不支援處理token的Realm跳過。如果遇到異常,忽略對異常的處理。在迴圈處理Realm前aggregate=null,重點是在strategy.afterAttempt(realm, token, info, aggregate, t)的處理上,並不會合併info和aggregate。如果aggregate為空,則返回info。所以在處理後返回的總是第一個認證成功的AuthenticationInfo。

總結

Authenticator負責對AuthenticationToken進行認證,然後返回一個已經被認證過的資訊AuthenticationInfo。Authenticator也提供了監聽器AuthenticationListener,對認證狀態進行監聽。Authenticator真實的認證過程是由Realm來處理的,可以支援都多個Realm來認證,認證的過程中可以選擇不同的認證策略。