厲害啊!第一次見到把Shiro運行流程寫的這麼清楚的,建議收藏起來慢慢看

前言

shiro是apache的一個開源框架,是一個權限管理的框架,實現 用戶認證、用戶受權。
spring中有spring security (原名Acegi),是一個權限框架,它和spring依賴過於緊密,沒有shiro使用簡單。
shiro不依賴於spring,shiro不只能夠實現 web應用的權限管理,還能夠實現c/s系統,分佈式系統權限管理,shiro屬於輕量框架,愈來愈多企業項目開始使用shiro。web

Shiro運行流程學習筆記

項目中使用到了shiro,因此對shiro作一些比較深的瞭解。面試

也不知從何瞭解起,先從shiro的運行流程開始。spring

運行流程

  1. 首先調用 Subject.login(token) 進行登陸,其會自動委託給 Security Manager,調用以前必須經過 SecurityUtils.setSecurityManager() 設置;
  2. SecurityManager 負責真正的身份驗證邏輯;它會委託給 Authenticator 進行身份驗證;
  3. Authenticator 纔是真正的身份驗證者,Shiro API 中核心的身份認證入口點,此處能夠自定義插入本身的實現;
  4. Authenticator 可能會委託給相應的 AuthenticationStrategy 進行多 Realm 身份驗證,默認 ModularRealmAuthenticator 會調用 AuthenticationStrategy 進行多 Realm 身份驗證;
  5. Authenticator 會把相應的 token 傳入 Realm,從 Realm 獲取身份驗證信息,若是沒有返回 / 拋出異常表示身份驗證失敗了。此處能夠配置多個 Realm,將按照相應的順序及策略進行訪問。

綁定線程

這裏從看項目源碼開始。數據庫

看第一步,Subject.login(token)方法。apache

UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
subject.login(token);

出現了一個UsernamePasswordToken對象,它在這裏會調用它的一個構造函數。設計模式

public UsernamePasswordToken(final String username, final String password, final boolean rememberMe) {
    this(username, password != null ? password.toCharArray() : null, rememberMe, null);
}

據筆者本身瞭解,這是shiro的一個驗證對象,只是用來存儲用戶名密碼,以及一個記住我屬性的。安全

以後會調用shiro的一個工具類獲得一個subject對象。app

public static Subject getSubject() {
    Subject subject = ThreadContext.getSubject();
    if (subject == null) {
        subject = (new Subject.Builder()).buildSubject();
        ThreadContext.bind(subject);
    }
    return subject;
}

經過getSubject方法來獲得一個Subject對象。框架

這裏不得不提到shiro的內置線程類ThreadContext,經過bind方法會將subject對象綁定在線程上。分佈式

public static void bind(Subject subject) {
    if (subject != null) {
        put(SUBJECT_KEY, subject);
    }
}
public static void put(Object key, Object value) {
    if (key == null) {
        throw new IllegalArgumentException("key cannot be null");
    }

    if (value == null) {
        remove(key);
        return;
    }

    ensureResourcesInitialized();
    resources.get().put(key, value);

    if (log.isTraceEnabled()) {
        String msg = "Bound value of type [" + value.getClass().getName() + "] for key [" +
                key + "] to thread " + "[" + Thread.currentThread().getName() + "]";
        log.trace(msg);
    }
}

shirokey都是遵循一個固定的格式。

public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";

通過非空判斷後會將值以KV的形式put進去。

當你想拿到subject對象時,也能夠經過getSubject方法獲得subject對象。

在綁定subject對象時,也會將securityManager對象進行一個綁定。

而綁定securityManager對象的地方是在Subject類的一個靜態內部類裏(可以讓我好一頓找)。

getSubject方法中的一句代碼調用了內部類的buildSubject方法。

subject = (new Subject.Builder()).buildSubject();

PS:此處運用到了建造者設計模式,能夠去菜鳥教程仔細瞭解

進去觀看源碼後能夠看見。

首先調用無參構造,在無參構造裏調用有參構造函數。

public Builder() {
    this(SecurityUtils.getSecurityManager());
}

public Builder(SecurityManager securityManager) {
    if (securityManager == null) {
        throw new NullPointerException("SecurityManager method argument cannot be null.");
    }
    this.securityManager = securityManager;
    this.subjectContext = newSubjectContextInstance();
    if (this.subjectContext == null) {
        throw new IllegalStateException("Subject instance returned from 'newSubjectContextInstance' " +
                "cannot be null.");
    }
    this.subjectContext.setSecurityManager(securityManager);
}

在此處綁定了securityManager對象。

固然,他也對securityManager對象的空情況進行了處理,在getSecurityManager方法裏。

public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException {
    SecurityManager securityManager = ThreadContext.getSecurityManager();
    if (securityManager == null) {
        securityManager = SecurityUtils.securityManager;
    }
    if (securityManager == null) {
        String msg = "No SecurityManager accessible to the calling code, either bound to the " +
                ThreadContext.class.getName() + " or as a vm static singleton.  This is an invalid application " +
                "configuration.";
        throw new UnavailableSecurityManagerException(msg);
    }
    return securityManager;
}

真正的核心就在於securityManager這個對象。

SecurityManager

SecurityManager是一個接口,他繼承了步驟裏所談到的AuthenticatorAuthorizer類以及用於Session管理的SessionManager

public interface SecurityManager extends Authenticator, Authorizer, SessionManager {

    Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;   

    void logout(Subject subject);

    Subject createSubject(SubjectContext context);
}

看一下它的實現。

且這些類和接口都有依次繼承的關係。

Relam

接下來了解一下另外一個重要的概念Relam

Realm充當了Shiro與應用安全數據間的「橋樑」或者「鏈接器」。也就是說,當與像用戶賬戶這類安全相關數據進行交互,執行認證(登陸)和受權(訪問控制)時,Shiro會從應用配置的Realm中查找不少內容。

從這個意義上講,Realm實質上是一個安全相關的DAO:它封裝了數據源的鏈接細節,並在須要時將相關數據提供給Shiro。當配置Shiro時,你必須至少指定一個Realm,用於認證和(或)受權。配置多個Realm是能夠的,可是至少須要一個。

Shiro內置了能夠鏈接大量安全數據源(又名目錄)的Realm,如LDAP、關係數據庫(JDBC)、相似INI的文本配置資源以及屬性文件 等。若是缺省的Realm不能知足需求,你還能夠插入表明自定義數據源的本身的Realm實現。

通常狀況下,都會自定義Relam來使用。

先看一下實現。

以及自定義的一個UserRelam

看一下類圖。

每一個抽象類繼承後所須要實現的方法都不同。

public class UserRealm extends AuthorizingRealm

這裏繼承AuthorizingRealm,須要實現它的兩個方法。

//給登陸用戶受權
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

//這個抽象方法屬於AuthorizingRealm抽象類的父類AuthenticatingRealm類   登陸認證,也是登陸的DAO操做所在的方法
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;

以後再來看看這個驗證方法,在以前的步驟裏提到了,驗證用到了Authenticator ,也就是第五步。

Authenticator

Authenticator 會把相應的 token 傳入 Realm,從 Realm 獲取身份驗證信息,若是沒有返回 / 拋出異常表示身份驗證失敗了。此處能夠配置多個 Realm,將按照相應的順序及策略進行訪問。

再回到以前登陸方法上來看看。

subject.login(token)在第一步中調用了Subjectlogin方法,找到它的最終實現DelegatingSubject類。

裏面有調用了securityManagerlogin方法,而最終實現就在DefaultSecurityManager這個類裏。

Subject subject = securityManager.login(this, token);
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        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
    }

以後就是驗證流程,這裏咱們會看到第四步,點進去會到抽象類AuthenticatingSecurityManager。再看看它的仔細調用。

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

真正的調用Relam進行驗證並不在這,而是在ModularRealmAuthenticator

他們之間是一個從左到右的過程。

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

在這裏我們就看這個doSingleRealmAuthentication方法。

Relam驗證。

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);
    }
    //在此處調用你自定義的Relam的方法來驗證。
    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;
}

再看看多Relam的。

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

    AuthenticationStrategy strategy = getAuthenticationStrategy();

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

    for (Realm realm : realms) {

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

        if (realm.supports(token)) {

            AuthenticationInfo info = null;
            Throwable t = null;
            try {
                //調用自定義的Relam的方法來驗證。
                info = realm.getAuthenticationInfo(token);
            } catch (Throwable throwable) {
                t = throwable;
            }

            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;
}

會發現調用的都是RelamgetAuthenticationInfo方法。

看到了熟悉的UserRelam,此致,閉環了。

可是也只是瞭解了大概的流程,對每一個類的具體做用並非很瞭解,因此筆者仍是有不少地方要去學習,不,應該說我原本就是菜雞,就要學才能變帶佬。

最後

你們看完有什麼不懂的能夠在下方留言討論,也能夠關注我私信問我,我看到後都會回答的。也歡迎你們關注個人公衆號:前程有光,立刻金九銀十跳槽面試季,整理了1000多道將近500多頁pdf文檔的Java面試題資料放在裏面,助你圓夢BAT!文章都會在裏面更新,整理的資料也會放在裏面。謝謝你的觀看,以爲文章對你有幫助的話記得關注我點個贊支持一下!

相關文章
相關標籤/搜索