Shiro源碼分析----登陸流程

在Shiro中,登陸操做是由Subject的login()方法完成的,Subject是個接口,在Web環境中,實現類爲WebDelegatingSubjectlogin方法從DeletatingSubject繼承而來:java

public void login(AuthenticationToken token) throws AuthenticationException {
    clearRunAsIdentitiesInternal();
    Subject subject = securityManager.login(this, token);

    // 省略一些代碼...
}

由上可見,Subject.login()方法委託給了SecurityManager對象,在Web環境中,SecurityManager實現類爲DefaultWebSecurityManager,其login方法從DefaultSecurityManager繼承而來:數據庫

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        // 對提交的AuthenticationToken進行認證
        info = authenticate(token);
    } catch (AuthenticationException ae) {
        try {
            // 若是認證失敗
            onFailedLogin(token, ae, subject);
        } catch (Exception e) {
            if (log.isInfoEnabled()) {
                log.info("onFailedLogin method threw an " +
                        "exception.  Logging and propagating original AuthenticationException.", e);
            }
        }
        throw ae; //propagate,若是認證失敗,使異常繼續向上傳播,從而返回至登陸頁面(見上篇)
    }

    // 若是認證成功則從新建立Subject對象
    Subject loggedIn = createSubject(token, info, subject);

    // 登陸成功,主要處理RememberMe操做,即將登陸信息存儲在cookie中
    onSuccessfulLogin(token, info, loggedIn);

    return loggedIn;
}

最關鍵的authenticate方法:設計模式

public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    return this.authenticator.authenticate(token);
}

SecurityManager把認證方法委託給認證器Authenticatorauthenticate方法,Authenticator的實現類爲:ModularRealmAuthenticator,其能夠實現多認證信息源綜合認證。ModularRealmAuthenticator實現使用了模版方法設計模式,隨後執行doAuthenticate方法:緩存

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    // Realm集合在爲SecurityManager設置Realm時就會設置給Authenticator
    // 至於Realm表明什麼,請參看:http://jinnianshilongnian.iteye.com/blog/2018936
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
        // 若是Realm只有一個
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        // 若是Realm有多個
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

單一Realm認證:cookie

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    // 該Realm是否支持此種Token,由於並非任何一種Realm與AuthenticationToken都是相互匹配的
    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);
    }
    // 根據AuthenticationToken獲取認證信息
    // Realm通常是由本身實現的,雖說Shiro有一些本身的實現,可是在實際項目中,Shiro的實現直接就能使用的狀況不多
    // 比較將認證信息(用戶名密碼等)存在數據庫,則該getAuthenticationInfo方法就是根據Token中的信息去數據庫中查找、
    // 匹配,若是匹配上了則返回相應認證後的認證信息
    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會繼承自AuthenticatingRealm,因此會執行至:app

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) {
        // 斷言AuthenticationToken與AuthenticationInfo是匹配的,簡單點來講就是判斷密碼是否正確,不正確則拋異常
        // doGetAuthenticationInfo方法主要判斷帳戶是否存在
        assertCredentialsMatch(token, info);
    } else {
        log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
    }

    return info;
}

單一Realm認證時,只須要判斷一個Realm認證是否成功便可,可是當存在多個Realm時狀況就有點複雜了。由於有可能有些Realm認證成功了,有些Realm又認證失敗了,這時到底算是認證成功仍是失敗呢?因此這時Shiro使用了策略模式,用具體的策略類來處理這個問題。多個Realm認證時的doMultiRealmAuthentication方法以下:ide

protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
    // 首先就得獲取認證策略,Shiro實現了三種:
    //1. AllSuccessfulStrategy: 必須全部Realm認證成功了纔算是認證成功
    //2. AtLeastOneSuccessfulStrategy: 至少有一個Realm認證成功了就算是認證成功
    //3. FirstSuccessfulStrategy: 第一個Realm認證成功了就算是認證成功
    // 默認實現爲AtLeastOneSuccessfulStrategy
    AuthenticationStrategy strategy = getAuthenticationStrategy();

    // 假設咱們如今使用的就是AtLeastOneSuccessfulStrategy
    // 返回SimpleAuthenticationInfo,這是一個空認證信息,並不含有principal與credentials
    AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);

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

    for (Realm realm : realms) {
        // 直接返回aggregate
        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);
                }
            }
            //若是認證成功則info不爲null,且包含有principal與credentials
            //afterAttempt方法會將info與aggregate合併,也就是將AuthenticationInfo的principal與credentials
            //分別用一集合存儲
            aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);

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

    // 檢測合併後的AuthenticationInfo中是否含用principal,若是有則返回aggregate
    // 沒有則拋出異常認證失敗,因而可知只要有一個Realm認證成功則算是認證成功
    aggregate = strategy.afterAllAttempts(token, aggregate);

    return aggregate;
}

上面只分析了AtLeastOneSuccessfulStrategy策略,其它兩個請自行查看源碼。this

假設如今認證成功了,接下來執行DefaultSecurityManager.createSubject方法:debug

protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
    // 建立SubjectContext對象
    SubjectContext context = createSubjectContext();
    // 設置爲已認證
    context.setAuthenticated(true);
    // 設置Token
    context.setAuthenticationToken(token);
    // 設置認證經過後的認證信息
    context.setAuthenticationInfo(info);
    if (existing != null) {
        // 設置先前存在的Subject
        context.setSubject(existing);
    }
    return createSubject(context);
}

public Subject createSubject(SubjectContext subjectContext) {
    // 複製SubjectContext,原SubjectContext信息得以保留
    SubjectContext context = copy(subjectContext);

    // 確保SubjectContext與SecurityManager關聯
    context = ensureSecurityManager(context);

    // 解析會話,有可能使用Servlet中的Session實現,也可能使用Shiro本身實現的Session
    context = resolveSession(context);

    context = resolvePrincipals(context);

    // 交由DefaultWebSubjectFactory.createSubject從新建立Subject
    Subject subject = doCreateSubject(context);

    // 將Subject中的principal與credentials存儲在Session中
    save(subject);

    return subject;
}

-------------------------------- END -------------------------------設計

及時獲取更多精彩文章,請關注公衆號《Java精講》。

相關文章
相關標籤/搜索