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

前言

shiro是apache的一個開源框架,是一個權(quán)限管理的框架析蝴,實現(xiàn) 用戶認證害捕、用戶授權(quán)。
spring中有spring security (原名Acegi)闷畸,是一個權(quán)限框架尝盼,它和spring依賴過于緊密,沒有shiro使用簡單佑菩。
shiro不依賴于spring盾沫,shiro不僅可以實現(xiàn) web應(yīng)用的權(quán)限管理谅年,還可以實現(xiàn)c/s系統(tǒng)摩桶,分布式系統(tǒng)權(quán)限管理,shiro屬于輕量框架甥绿,越來越多企業(yè)項目開始使用shiro绞幌。

Shiro運行流程學(xué)習(xí)筆記

項目中使用到了shiro蕾哟,所以對shiro做一些比較深的了解。

也不知從何了解起莲蜘,先從shiro的運行流程開始渐苏。

運行流程

  1. 首先調(diào)用 Subject.login(token) 進行登錄,其會自動委托給 Security Manager菇夸,調(diào)用之前必須通過 SecurityUtils.setSecurityManager() 設(shè)置;
  2. SecurityManager 負責(zé)真正的身份驗證邏輯仪吧;它會委托給 Authenticator 進行身份驗證庄新;
  3. Authenticator 才是真正的身份驗證者,Shiro API 中核心的身份認證入口點薯鼠,此處可以自定義插入自己的實現(xiàn)择诈;
  4. Authenticator 可能會委托給相應(yīng)的 AuthenticationStrategy 進行多 Realm 身份驗證,默認 ModularRealmAuthenticator 會調(diào)用 AuthenticationStrategy 進行多 Realm 身份驗證出皇;
  5. Authenticator 會把相應(yīng)的 token 傳入 Realm羞芍,從 Realm 獲取身份驗證信息,如果沒有返回 / 拋出異常表示身份驗證失敗了郊艘。此處可以配置多個 Realm荷科,將按照相應(yīng)的順序及策略進行訪問唯咬。

綁定線程

這里從看項目源碼開始。

看第一步畏浆,Subject.login(token)方法胆胰。

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

出現(xiàn)了一個UsernamePasswordToken對象,它在這里會調(diào)用它的一個構(gòu)造函數(shù)刻获。

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

據(jù)筆者自己了解蜀涨,這是shiro的一個驗證對象,只是用來存儲用戶名密碼蝎毡,以及一個記住我屬性的厚柳。

之后會調(diào)用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的內(nèi)置線程類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";

經(jīng)過非空判斷后會將值以KV的形式put進去宰闰。

當(dāng)你想拿到subject對象時,也可以通過getSubject方法得到subject對象簿透。

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

而綁定securityManager對象的地方是在Subject類的一個靜態(tài)內(nèi)部類里(可讓我好一頓找)老充。

getSubject方法中的一句代碼調(diào)用了內(nèi)部類的buildSubject方法葡盗。

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

PS:此處運用到了建造者設(shè)計模式,可以去菜鳥教程仔細了解

進去觀看源碼后可以看見啡浊。

首先調(diào)用無參構(gòu)造觅够,在無參構(gòu)造里調(diào)用有參構(gòu)造函數(shù)。

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對象巷嚣。

當(dāng)然喘先,他也對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是一個接口,他繼承了步驟里所談到的Authenticator坝茎,Authorizer類以及用于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);
}

看一下它的實現(xiàn)。

且這些類和接口都有依次繼承的關(guān)系嗤放。

Relam

接下來了解一下另一個重要的概念Relam思喊。

Realm充當(dāng)了Shiro與應(yīng)用安全數(shù)據(jù)間的“橋梁”或者“連接器”。也就是說次酌,當(dāng)與像用戶帳戶這類安全相關(guān)數(shù)據(jù)進行交互恨课,執(zhí)行認證(登錄)和授權(quán)(訪問控制)時舆乔,Shiro會從應(yīng)用配置的Realm中查找很多內(nèi)容。

從這個意義上講庄呈,Realm實質(zhì)上是一個安全相關(guān)的DAO:它封裝了數(shù)據(jù)源的連接細節(jié)蜕煌,并在需要時將相關(guān)數(shù)據(jù)提供給Shiro。當(dāng)配置Shiro時诬留,你必須至少指定一個Realm斜纪,用于認證和(或)授權(quán)。配置多個Realm是可以的文兑,但是至少需要一個盒刚。

Shiro內(nèi)置了可以連接大量安全數(shù)據(jù)源(又名目錄)的Realm,如LDAP绿贞、關(guān)系數(shù)據(jù)庫(JDBC)因块、類似INI的文本配置資源以及屬性文件 等。如果缺省的Realm不能滿足需求籍铁,你還可以插入代表自定義數(shù)據(jù)源的自己的Realm實現(xiàn)涡上。

一般情況下,都會自定義Relam來使用拒名。

先看一下實現(xiàn)吩愧。

以及自定義的一個UserRelam

看一下類圖增显。

每個抽象類繼承后所需要實現(xiàn)的方法都不一樣雁佳。

public class UserRealm extends AuthorizingRealm

這里繼承AuthorizingRealm,需要實現(xiàn)它的兩個方法同云。

//給登錄用戶授權(quán)
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

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

之后再來看看這個驗證方法,在之前的步驟里提到了炸站,驗證用到了Authenticator 星澳,也就是第五步。

Authenticator

Authenticator 會把相應(yīng)的 token 傳入 Realm旱易,從 Realm 獲取身份驗證信息募判,如果沒有返回 / 拋出異常表示身份驗證失敗了。此處可以配置多個 Realm咒唆,將按照相應(yīng)的順序及策略進行訪問。

再回到之前登錄方法上來看看释液。

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

里面有調(diào)用了securityManagerlogin方法误债,而最終實現(xiàn)就在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李命。再看看它的仔細調(diào)用登淘。

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

真正的調(diào)用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);
    }
    //在此處調(diào)用你自定義的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 {
                //調(diào)用自定義的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;
}

會發(fā)現(xiàn)調(diào)用的都是RelamgetAuthenticationInfo方法绅这。

看到了熟悉的UserRelam,此致在辆,閉環(huán)了证薇。

但是也只是了解了大概的流程,對每個類的具體作用并不是很了解匆篓,所以筆者還是有很多地方要去學(xué)習(xí)浑度,不,應(yīng)該說我本來就是菜雞奕删,就要學(xué)才能變帶佬俺泣。

最后

感謝你看到這里,看完有什么的不懂的可以在評論區(qū)問我完残,覺得文章對你有幫助的話記得給我點個贊伏钠,每天都會分享java相關(guān)技術(shù)文章或行業(yè)資訊,歡迎大家關(guān)注和轉(zhuǎn)發(fā)文章谨设!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末熟掂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子扎拣,更是在濱河造成了極大的恐慌赴肚,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件二蓝,死亡現(xiàn)場離奇詭異誉券,居然都是意外死亡,警方通過查閱死者的電腦和手機刊愚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進店門踊跟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鸥诽,你說我怎么就攤上這事商玫』叮” “怎么了?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵拳昌,是天一觀的道長袭异。 經(jīng)常有香客問我,道長炬藤,這世上最難降的妖魔是什么御铃? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮刻像,結(jié)果婚禮上畅买,老公的妹妹穿的比我還像新娘。我一直安慰自己细睡,他們只是感情好谷羞,可當(dāng)我...
    茶點故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著溜徙,像睡著了一般湃缎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蠢壹,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天嗓违,我揣著相機與錄音,去河邊找鬼图贸。 笑死蹂季,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的疏日。 我是一名探鬼主播偿洁,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼沟优!你這毒婦竟也來了涕滋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤挠阁,失蹤者是張志新(化名)和其女友劉穎宾肺,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侵俗,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡锨用,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了隘谣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片增拥。...
    茶點故事閱讀 38,697評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出跪者,到底是詐尸還是另有隱情,我是刑警寧澤熄求,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布渣玲,位于F島的核電站,受9級特大地震影響弟晚,放射性物質(zhì)發(fā)生泄漏忘衍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一卿城、第九天 我趴在偏房一處隱蔽的房頂上張望枚钓。 院中可真熱鬧,春花似錦瑟押、人聲如沸搀捷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嫩舟。三九已至,卻和暖如春怀偷,著一層夾襖步出監(jiān)牢的瞬間家厌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工椎工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留饭于,地道東北人。 一個月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓维蒙,卻偏偏與公主長得像掰吕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子木西,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,587評論 2 350