1. 程式人生 > 實用技巧 >厲害啊!第一次見到把Shiro執行流程寫的這麼清楚的,建議收藏起來慢慢看

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

前言

shiro是apache的一個開源框架,是一個許可權管理的框架,實現 使用者認證、使用者授權。
spring中有spring security (原名Acegi),是一個許可權框架,它和spring依賴過於緊密,沒有shiro使用簡單。
shiro不依賴於spring,shiro不僅可以實現 web應用的許可權管理,還可以實現c/s系統,分散式系統許可權管理,shiro屬於輕量框架,越來越多企業專案開始使用shiro。

Shiro執行流程學習筆記

專案中使用到了shiro,所以對shiro做一些比較深的瞭解。

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

執行流程

  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)方法。

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物件。

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!文章都會在裡面更新,整理的資料也會放在裡面。謝謝你的觀看,覺得文章對你有幫助的話記得關注我點個贊支援一下!