Shiro 登錄認證源碼詳解

文章轉(zhuǎn)載自:http://blog.csdn.net/w1196726224/article/details/53560385

Apache Shiro 是一個強大且靈活的 Java 開源安全框架沟堡,擁有登錄認證、授權(quán)管理、企業(yè)級會話管理和加密等功能禽最,相比 spring Security 來說要更加的簡單。
本文主要介紹 Shiro 的登錄認證(Authentication)功能孽亲,主要從 Shiro 設(shè)計的角度去看這個登錄認證的過程欲账。
一、Shiro 總覽
首先沟优,我們思考整個認證過程的業(yè)務(wù)邏輯:
獲取用戶輸入的用戶名,密碼睬辐;
從服務(wù)器數(shù)據(jù)源中獲取相應(yīng)的用戶名和密碼挠阁;
判斷密碼是否匹配,決定是否登錄成功溯饵。

我們現(xiàn)在來看看 Shiro 是如何設(shè)計這個過程的:

圖中包含三個重要的 Shiro 概念:Subject
侵俗、SecurityManager
、Realm
丰刊。接下來隘谣,分別介紹這三者有何用:
Subject:表示“用戶”,表示當(dāng)前執(zhí)行的用戶啄巧。Subject
實例全部都綁定到了一個 SecurityManager
上寻歧,當(dāng)和 Subject
交互時掌栅,它是委托給 SecurityManager
去執(zhí)行的。
SecurityManager:Shiro 結(jié)構(gòu)的心臟,協(xié)調(diào)它內(nèi)部的安全組件(如登錄,授權(quán)赂蠢,數(shù)據(jù)源等)手趣。當(dāng)整個應(yīng)用配置好了以后,大多數(shù)時候都是直接和 Subject
的 API 打交道。
Realm:數(shù)據(jù)源,也就是抽象意義上的 DAO 層。它負責(zé)和安全數(shù)據(jù)交互(比如存儲在數(shù)據(jù)庫的賬號枚钓、密碼,權(quán)限等信息)瑟押,包括獲取和驗證搀捷。Shiro 支持多個 Realm,但是至少也要有一個多望。Shiro 自帶了很多開箱即用的 Reams嫩舟,比如支持 LDAP、關(guān)系數(shù)據(jù)庫(JDBC)怀偷、INI 和 properties 文件等家厌。但是很多時候我們都需要實現(xiàn)自己的 Ream 去完成獲取數(shù)據(jù)和判斷的功能。

登錄驗證的過程就是:Subject
執(zhí)行 login
方法椎工,傳入登錄的「用戶名」和「密碼」饭于,然后SecurityManager
將這個 login
操作委托給內(nèi)部的登錄模塊,登錄模塊就調(diào)用 Realm
去獲取安全的「用戶名」和「密碼」维蒙,然后對比掰吕,一致則登錄,不一致則登錄失敗颅痊。
Shiro 詳細結(jié)構(gòu)

ShiroArchitecture

二殖熟、Shiro 登錄示例
代碼來自 Shiro 官網(wǎng)教程。Shiro 配置 INI 文件:

----------------------------------------------------------------------------# Users and their (optional) assigned roles# username = password, role1, role2, ..., roleN# ----------------------------------------------------------------------------[users]wang=123

1
2
3
4
5
6

1
2
3
4
5
6

測試 main 方法:
public static void main(String[] args) { log.info("My First Apache Shiro Application"); //1.從 Ini 配置文件中獲取 SecurityManager 工廠 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //2.獲取 SecurityManager 實例 SecurityManager securityManager = factory.getInstance(); //3.將 SecurityManager 實例綁定給 SecurityUtils SecurityUtils.setSecurityManager(securityManager); //4.獲取當(dāng)前登錄用戶 Subject currentUser = SecurityUtils.getSubject(); //5.判斷是否登錄斑响,如果未登錄吗讶,則登錄 if (!currentUser.isAuthenticated()) { //6.創(chuàng)建用戶名/密碼驗證Token(Web 應(yīng)用中即為前臺獲取的用戶名/密碼) UsernamePasswordToken token = new UsernamePasswordToken("wang", "123"); try { //7.執(zhí)行登錄,如果登錄未成功恋捆,則捕獲相應(yīng)的異常 currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

三、登錄邏輯詳解
Shiro 登錄過程主要涉及到 Subject.login
方法重绷,接下來我們將通過查看源碼來分析整個登錄過程沸停。
創(chuàng)建 AuthenticationToken
接口的實例 token,比如例子中的 UsernamePasswordToken
昭卓,包含了登錄的用戶名和密碼愤钾;
獲取當(dāng)前用戶 Subject
瘟滨,然后調(diào)用 Subject.login(AuthenticationToken)
方法;
Subject
將 login
代理給 SecurityManager
的 login()

**3.1 創(chuàng)建AuthenticationToken
**
第一步是創(chuàng)建 AuthenticationToken
接口的身份 token能颁,比如例子中的 UsernamePasswordToken
杂瘸。
package org.apache.shiro.authc;public interface AuthenticationToken extends Serializable { // 獲取“用戶名” Object getPrincipal(); // 獲取“密碼” Object getCredentials();}
1
2
3
4
5
6
7
8

1
2
3
4
5
6
7
8

3.2 獲取當(dāng)前用戶并執(zhí)行登錄
獲取的 Subject
當(dāng)前用戶是我們平時打交道最多的接口,有很多方法伙菊,但是這里我們只分析 login
方法败玉。
package org.apache.shiro.subject;public interface Subject { void login(AuthenticationToken token) throws AuthenticationException;}
1
2
3
4
5
6
7

1
2
3
4
5
6
7

login
方法接受一個 AuthenticationToken
參數(shù),如果登錄失敗則拋出 AuthenticationException
異常镜硕,可通過判斷異常類型來知悉具體的錯誤類型运翼。
接下來,分析 Subject
接口的實現(xiàn)類 DelegatingSubject
是如何實現(xiàn) login
方法的:
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); // 代理給SecurityManager Subject subject = securityManager.login(this, token); ...}
1
2
3
4
5
6

1
2
3
4
5
6

3.3 SecurityManager 接口
前面說過兴枯,整個 Shiro 安全框架的心臟就是 SecurityManager血淌,我們看這個接口都有哪些方法:
package org.apache.shiro.mgt;public interface SecurityManager extends Authenticator, Authorizer, SessionManager { Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException; void logout(Subject subject); Subject createSubject(SubjectContext context);}
1
2
3
4
5
6
7
8
9
10

1
2
3
4
5
6
7
8
9
10

SecurityManager 包含很多內(nèi)置的模塊來完成功能,比如登錄(Authenticator
)财剖,權(quán)限驗證(Authorizer
)等悠夯。這里我們看到 SecurityManager 接口繼承了 Authenticator
登錄認證的接口:
package org.apache.shiro.authc;public interface Authenticator { public AuthenticationInfo authenticate(AuthenticationToken authenticationToken) throws AuthenticationException;}
1
2
3
4
5
6
7

1
2
3
4
5
6
7

那么,SecurityManager
的實現(xiàn)都是怎樣來實現(xiàn) Authenticator
接口的呢躺坟?答案是:使用了組合沦补。SecurityManager 都擁有一個 Authenticator
的屬性,這樣**調(diào)用 SecurityManager.authenticate
** 的時候瞳氓,是委托給內(nèi)部的 Authenticator
屬性去執(zhí)行的策彤。

SecurityManager

3.4 SecurityManager.login 的實現(xiàn)
// DefaultSecurityManager.javapublic 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 } Subject loggedIn = createSubject(token, info, subject); onSuccessfulLogin(token, info, loggedIn); return loggedIn;}// AuthenticatingSecurityManager.java/** * Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication. */public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

調(diào)用自己的 authenticate
方法執(zhí)行登錄;
在 authenticate
方法中代理給 Authenticator
接口類型的屬性去真正執(zhí)行 authenticate(token)
方法匣摘。

3.5 Authenticator 登錄模塊
Authenticator
接口如下:
package org.apache.shiro.authc;public interface Authenticator { public AuthenticationInfo authenticate(AuthenticationToken authenticationToken) throws AuthenticationException;}
1
2
3
4
5
6
7

1
2
3
4
5
6
7

其實現(xiàn)類有 AbstractAuthenticator
和 ModularRealmAuthenticator

Authenticator

下面來看看如何實現(xiàn)的 authenticate
方法:
// AbstractAuthenticator.javapublic final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { // 調(diào)用doAuthenticate方法 info = doAuthenticate(token); if (info == null) { ... } } catch (Throwable t) { ... } ...}// ModularRealmAuthenticator.javaprotected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { // Realm唯一時 return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); }}protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { if (!realm.supports(token)) { ... } // 調(diào)用Realm的getAuthenticationInfo方法獲取AuthenticationInfo信息 AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { ... } return info;}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

從源碼中可以看出店诗,最后會調(diào)用 Realm
的 getAuthenticationInfo(AuthenticationToken)
方法。
3.6 Realm 接口
Realm 相當(dāng)于數(shù)據(jù)源音榜,功能是通過 AuthenticationToken
獲取數(shù)據(jù)源中的安全數(shù)據(jù)庞瘸,這個過程中可以拋出異常,告訴 shiro 登錄失敗赠叼。
package org.apache.shiro.realm;public interface Realm { // 獲取 shiro 唯一的 realm 名稱 String getName(); // 是否支持給定的 AuthenticationToken 類型 boolean supports(AuthenticationToken token); // 獲取 AuthenticationInfo AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;}
1
2
3
4
5
6
7
8
9
10
11
12
13

1
2
3
4
5
6
7
8
9
10
11
12
13

Shiro 自帶了很多開箱即用的 Realm 實現(xiàn)擦囊,具體的類圖如下:

Realm

3.7 總結(jié)
到此,我們把整個 Shiro 的登錄認證流程分析了一遍嘴办。
創(chuàng)建 AuthenticationToken
瞬场,然后調(diào)用 Subject.login
方法進行登錄認證;
Subject
委托給 SecurityManager
涧郊;
SecurityManager
委托給 Authenticator
接口贯被;
Authenticator
接口調(diào)用 Realm
獲取登錄信息。

整個過程中,如果登錄失敗彤灶,就拋出異常看幼,是使用異常來進行邏輯控制的。
四幌陕、登錄密碼的存儲
頁面使用 Https 協(xié)議诵姜;
頁面?zhèn)魉兔艽a時要先加密后再傳輸,最好是不可逆的加密算法(MD5搏熄,SHA2)棚唆;
后端存儲時要結(jié)合鹽(隨機數(shù))一起加密存儲;
使用不可逆的加密算法搬卒,而且可以加密多次瑟俭;
把加密后的密碼和鹽一起存儲到數(shù)據(jù)庫;

五契邀、學(xué)習(xí) Shiro 源碼感悟
從整體去思考框架的實現(xiàn)摆寄,帶著業(yè)務(wù)邏輯去看實現(xiàn)邏輯;
不要摳細節(jié)坯门,要看抽象微饥,學(xué)習(xí)其實現(xiàn)方法;
首先看官方文檔古戴,官方文檔一般會從整體設(shè)計方面去說明欠橘,遇到具體的接口再去看Javadoc文檔;
結(jié)合類圖等工具方便理解现恼;

六肃续、參考
Apache Shiro
跟我學(xué)Shiro目錄貼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市叉袍,隨后出現(xiàn)的幾起案子始锚,更是在濱河造成了極大的恐慌,老刑警劉巖喳逛,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞧捌,死亡現(xiàn)場離奇詭異,居然都是意外死亡润文,警方通過查閱死者的電腦和手機姐呐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來典蝌,“玉大人曙砂,你說我怎么就攤上這事】ハ疲” “怎么了麦轰?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵乔夯,是天一觀的道長。 經(jīng)常有香客問我款侵,道長,這世上最難降的妖魔是什么侧纯? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任新锈,我火速辦了婚禮,結(jié)果婚禮上眶熬,老公的妹妹穿的比我還像新娘妹笆。我一直安慰自己,他們只是感情好娜氏,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布拳缠。 她就那樣靜靜地躺著,像睡著了一般贸弥。 火紅的嫁衣襯著肌膚如雪窟坐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天绵疲,我揣著相機與錄音哲鸳,去河邊找鬼。 笑死盔憨,一個胖子當(dāng)著我的面吹牛徙菠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播郁岩,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼婿奔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了问慎?” 一聲冷哼從身側(cè)響起萍摊,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蝴乔,沒想到半個月后记餐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡薇正,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年片酝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挖腰。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡雕沿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出猴仑,到底是詐尸還是另有隱情审轮,我是刑警寧澤肥哎,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站疾渣,受9級特大地震影響篡诽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜榴捡,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一杈女、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吊圾,春花似錦达椰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至檀何,卻和暖如春蝇裤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背埃碱。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工猖辫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人砚殿。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓啃憎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親似炎。 傳聞我的和親對象是個殘疾皇子辛萍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容