shiro認證流程原始碼分析--練氣初期

賴柄灃發表於2020-10-05

寫在前面

在上一篇文章當中,我們通過一個簡單的例子,簡單地認識了一下shiro。在這篇文章當中,我們將通過閱讀原始碼的方式瞭解shiro的認證流程。

建議大家邊讀文章邊動手除錯程式碼,這樣效果會更好。

認證異常分析

shiro中的異常主要分為兩類,一類是AuthenticationException認證異常,一類是AuthorizationException許可權異常。分別對應http響應狀態碼中的401403

認證異常AuthenticationException子類

許可權異常AuthorizationException子類

當認證不通過時將根據具體情況丟擲AuthenticationException的子類,當鑑權不通過時將會丟擲AuthorizationException的子類。

我們通過檢驗shiro是否丟擲異常,從而判斷登入物件是否通過認證、是否具備相關保護資源的訪問許可權。

這也是我們在上一節的例子中,需要捕獲相關異常的原因。

接下來,我們通過閱讀原始碼的方式來分析一下shiro框架的認證流程。

認證流程分析

/**認證器
 * @author 賴柄灃 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/9/21 0:50
 */
public class Authenticator {

    private DefaultSecurityManager securityManager;

    public Authenticator(){
        //1. 建立安全管理器
        this.securityManager = new DefaultSecurityManager();

        //2. 給安全管理器設定問題域
        //因為許可權資訊從ini檔案中讀取,所以是IniRealm
        this.securityManager.setRealm(new IniRealm("classpath:shiro.ini"));

        //3. 注入安全管理器,並使用SecurityUtils全域性安全工具類完成認證
        SecurityUtils.setSecurityManager(securityManager);



    }

    /**認證
     * @author 賴柄灃 bingfengdev@aliyun.com
     * @date 2020-09-23 16:22:11
     * @param username 使用者名稱
     * @param password 密碼
     * @return void
     * @version 1.0
     */
    public void authenticate(String username,String password){
        //4. 獲取當前主題
        Subject subject = SecurityUtils.getSubject();

        //5.根據登入物件身份憑證資訊建立登入令牌
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        //6.認證
        //如果認證通過,則不丟擲異常,否則丟擲AuthenticationExceptixon異常子類
        //正式專案建議直接丟擲,統一異常處理
        try {
            subject.login(token);
        }catch (IncorrectCredentialsException e) {
            e.printStackTrace();
        }catch (ConcurrentAccessException e){
            e.printStackTrace();
        }catch (UnknownAccountException e){
            e.printStackTrace();
        }catch (ExcessiveAttemptsException e){
            e.printStackTrace();
        }catch (ExpiredCredentialsException e){
            e.printStackTrace();
        }catch (LockedAccountException e){
            e.printStackTrace();
        }

    }


}

這是上一個例子當中的認證器的程式碼。 我們在上述程式碼的44行,shiro認證的入口處打個斷點,以便跟蹤其認證流程。

然後在idea中以debug的形式啟動程式。

DelegatingSubject

login()方法

我們發現我們進入了DelegatingSubject.login方法當中;

public class DelegatingSubject implements Subject {
    //省略了其他不影響理解的程式碼
      public void login(AuthenticationToken token) throws AuthenticationException {
         
        this.clearRunAsIdentitiesInternal();
          // 1. 真正做認證的還是securityManager物件
        Subject subject = this.securityManager.login(this, token);
        String host = null;
        PrincipalCollection principals;
        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject)subject;
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }

        if (principals != null && !principals.isEmpty()) {
            this.principals = principals;
            this.authenticated = true;
            if (token instanceof HostAuthenticationToken) {
                host = ((HostAuthenticationToken)token).getHost();
            }

            if (host != null) {
                this.host = host;
            }

            Session session = subject.getSession(false);
            if (session != null) {
                this.session = this.decorate(session);
            } else {
                this.session = null;
            }

        } else {
            String msg = "Principals returned from securityManager.login( token ) returned a null or empty value.  This value must be non null and populated with one or more elements.";
            throw new IllegalStateException(msg);
        }
    }
 
}

從上面的原始碼中我們發現,雖然我們呼叫了Subject物件的認證方法,但是,真正的認證操作還是由安全管理器物件securityManager執行。

DefaultSecurityManager

login() 方法

接著,我們進入到securityManager的login方法當中去。

public class DefaultSecurityManager extends SessionsSecurityManager {


    //省略了其他無關程式碼
    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            //呼叫認證方法
            info = this.authenticate(token);
        } catch (AuthenticationException var7) {
            AuthenticationException ae = var7;

            try {
                this.onFailedLogin(token, ae, subject);
            } catch (Exception var6) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an exception.  Logging and propagating original AuthenticationException.", var6);
                }
            }

            throw var7;
        }

        Subject loggedIn = this.createSubject(token, info, subject);
        this.onSuccessfulLogin(token, info, loggedIn);
        return loggedIn;
    }

}

AuthenticatingSecurityManager

authenticate()方法

當我們進入到authenticate方法中時,發現他是AuthenticatingSecurityManager的方法

public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
//省略了其他無關程式碼
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    return this.authenticator.authenticate(token);
    }
}

AbstractAuthenticator

authenticate()方法

接著,他又呼叫了authenticator物件的authenticate方法

public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
    //省略了其他無關方法
     public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        if (token == null) {
            throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
        } else {
            log.trace("Authentication attempt received for token [{}]", token);

            AuthenticationInfo info;
            try {
              
                info = this.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 var8) {
                AuthenticationException ae = null;
                if (var8 instanceof AuthenticationException) {
                    ae = (AuthenticationException)var8;
                }

                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, var8);
                    if (log.isWarnEnabled()) {
                        log.warn(msg, var8);
                    }
                }

                try {
                    this.notifyFailure(token, ae);
                } catch (Throwable var7) {
                    if (log.isWarnEnabled()) {
                        String msg = "Unable to send notification for failed authentication attempt - listener error?.  Please check your AuthenticationListener implementation(s).  Logging sending exception and propagating original AuthenticationException instead...";
                        log.warn(msg, var7);
                    }
                }

                throw ae;
            }

            log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);
            this.notifySuccess(token, info);
            return info;
        }
    }
}

ModularRealmAuthenticator

doAuthenticate()方法

緊接著進入到了ModularRealmAuthenticator認證器物件的doAuthenticate方法

public class ModularRealmAuthenticator extends AbstractAuthenticator {
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        this.assertRealmsConfigured();
        Collection<Realm> realms = this.getRealms();
        return realms.size() == 1 ? 
 /**終於到了真正的認證邏輯*/            	this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
    }
}

在這一步當中,在檢驗我們的Realms物件建立後,開始進入到doSingleRealmAuthentication方法當中進行認證操作

doSingleRealmAuthentication()方法

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken 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);
        } else {
            //獲取認證資訊
            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);
            } else {
                return info;
            }
        }
    }

AuthenticatingRealm

getAuthenticationInfo()方法

在這一步當中開始根據我們傳入的令牌獲取認證資訊

public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 首先從快取中獲取
        AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
        if (info == null) {
            //快取中沒有,則從持久化資料中獲取
            info = this.doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                this.cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }

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

        return info;
    }
}

SimpleAccountRealm

doGetAuthenticationInfo()方法

從持久化資料來源中獲取登入物件資訊

public class SimpleAccountRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    UsernamePasswordToken upToken = (UsernamePasswordToken)token;
    //根據使用者名稱查詢賬戶資訊
    SimpleAccount account = this.getUser(upToken.getUsername());
    //如果查詢到了賬戶資訊
    if (account != null) {
        //開始判斷賬戶狀態
        if (account.isLocked()) {
            throw new LockedAccountException("Account [" + account + "] is locked.");
        }

        if (account.isCredentialsExpired()) {
            String msg = "The credentials for account [" + account + "] are expired";
            throw new ExpiredCredentialsException(msg);
        }
    }

    return account;
}
}

在這裡,便完成了對使用者名稱的校驗。

AuthenticatingRealm

接下來,我們獲取到了賬戶資訊並返回到了AuthenticatingRealm的getAuthenticationInfo方法。

在這個方法中有如下幾行程式碼,在第二行中,呼叫assertCredentialsMatch方法開始校驗使用者憑證

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

assertCredentialsMatch()方法

protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
    //獲取憑證匹配器物件
    CredentialsMatcher cm = this.getCredentialsMatcher();
    if (cm != null) {
        if (!cm.doCredentialsMatch(token, info)) {
            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.");
    }
}

在這裡完成對使用者憑證的校驗。真正的比較邏輯則在SimpleCredentialsMatcher的equals方法中完成。裡面還會區分加密和不加密的情況,具體請檢視原始碼。

分析到這一步我們可以發現,SimpleAccountRealm繼承了AuthorizingRealm類實現doGetAuthenticationInfo方法完成賬戶資訊查詢並校驗,並將結果返回給AuthorizingRealm。AuthorizingRealm幫SimpleAccountRealm完成對使用者憑證的校驗。

那麼,如果我們需要從資料庫當中獲取賬戶資訊,應該怎麼將賬戶資訊傳給shiro進行驗證呢?這個問題留給大家思考一下,我將在下一篇文章當中為大家解答。

寫在最後

在這篇文章當中,我們通過斷點除錯,閱讀原始碼的方式弄清楚了shiro的認證流程。我們拆開他的層層封裝,發現在SimpleAccountRealm物件中的doGetAuthenticationInfo方法中完成賬戶驗證,在AuthenticatingRealm的assertCredentialsMatch完成對使用者憑證的校驗。

在下一篇文章當中,我們將學習如何使用資料庫資訊完成認證和授權。

相關文章