認(rèn)證就是身份驗(yàn)證的過程,具體來說就是證明一個用戶的真實(shí)身份和用戶所描述的身份一致的過程蒜鸡。當(dāng)驗(yàn)證一個用戶身份的時候馆衔,需要用戶提供一些身份信息就比如系統(tǒng)可以理解的結(jié)構(gòu)化的數(shù)據(jù)。
這個過程通過提交一個用戶的principal和credential給shiro判斷是否匹配應(yīng)用內(nèi)部期望的信息來完成的扭弧。
- principal是一個用戶的身份屬性,principal可以有多個,可以是任何可證明用戶身份的東西,比如系統(tǒng)名(由系統(tǒng)給定的名稱),昵稱(用戶自定義名),用戶手機(jī)號,社保號碼,等等.當(dāng)然有些內(nèi)容并不適合作為principal,比如用戶實(shí)際的姓名,因?yàn)楝F(xiàn)實(shí)中重名的概率極大,最好的principal必須是唯一的,比如用戶的身份證號或者郵箱地址.
主principal
Shiro中可以存在任意數(shù)量的principal,但推薦一個應(yīng)用程序只接受一個主要的principal,一個在應(yīng)用中唯一存在的值對應(yīng)一個principal.這也是在大多數(shù)應(yīng)用中經(jīng)常使用的用戶名,郵箱地址或者全局唯一用戶id等單一principal認(rèn)證方式.
- credential通常是加密后的數(shù)據(jù),只和principal相關(guān)的內(nèi)容,總是被用來校驗(yàn)身份是否屬實(shí).通常的實(shí)例是密碼,生物數(shù)據(jù)數(shù)據(jù)比如指紋信息,視網(wǎng)膜掃描,和X.509格式的證書等.
比較常見的principal/credential對是用戶名和密碼.用戶名就是聲明的信息,密碼就是聲明的信息和系統(tǒng)內(nèi)部是否匹配的證明.如果提交的密碼與內(nèi)部存儲的期望的信息匹配就足以說明此用戶為真.
用戶證明
用戶認(rèn)證過程可以被分為三個步驟:
- 收集用戶提交的principal和credential
- 提交收集到的信息
-
如果后臺驗(yàn)證成功,則允許使用系統(tǒng)其它功能,否則需要重新認(rèn)證或者禁止認(rèn)證
接下來的代碼演示了如何使用Shiro的API實(shí)現(xiàn)上述步驟:
第一步: 收集用戶的principal和credential
// 通常情況下使用用戶名密碼進(jìn)行安全認(rèn)證的例子
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 自帶記住我的功能
token.setRememberMe(true);
在此案例中,我們使用了UsernamePasswordToken
類,支持最常見的用戶名/密碼的身份認(rèn)證方式.這個類實(shí)現(xiàn)了Shiro里面的org.apache.shiro.authc.AuthenticationToken
接口,AuthenticationToken接口的任意實(shí)現(xiàn)都可以被Shiro的認(rèn)證系統(tǒng)獲取此系統(tǒng)的使用者提交的principal和credential.
有一點(diǎn)很重要,Shiro根本不關(guān)心你是如何獲取這些信息的,或許這些數(shù)據(jù)信息是一個用戶通過html表單里面提交的,或者它檢索自一個HTTP頭信息,又或者它來源于一個Swing/Flex GUI密碼表單,又或者僅僅是命令行的參數(shù).從終端用戶那里收集信息的過程與認(rèn)證用戶身份的過程是完全分離的.
你可以隨意構(gòu)造和創(chuàng)建AuthenticationToken對象實(shí)例-它與協(xié)議無關(guān)
此示例還表明,我們已經(jīng)表示希望Shiro為身份驗(yàn)證嘗試執(zhí)行'Remember Me'服務(wù).這就保證了Shiro記住此用戶的登錄信息,同時可以在用戶一段時間后再次使用此應(yīng)用時無需重復(fù)登錄.我們將在之后的章節(jié)介紹Remember Me(記住我)服務(wù).
第二步:提交principal和credential
在principal和credential收集成功并創(chuàng)建了一個AuthenticationToken
對象實(shí)例后,我們需要提交此令牌給Shiro平臺執(zhí)行真正的認(rèn)證測試:
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
獲取到當(dāng)前正在訪問的用戶
后,用其調(diào)用一次login
(登錄)方法,將之前創(chuàng)建的AuthenticationToken
實(shí)例作為登錄方法的參數(shù)傳過去.
每次login
(登錄)方法的調(diào)用都意味著需要重新嘗試認(rèn)證一次.
第三步:處理成功和失敗
如果login
(登錄)方法平穩(wěn)的執(zhí)行成功了,這就意味著認(rèn)證結(jié)束了!Subject
(用戶)已經(jīng)被認(rèn)證了.此程序線程可以在沒有中斷的情況下繼續(xù)執(zhí)行,并且隨后任意次數(shù)的執(zhí)行SecurityUtils.getSubject()
方法都將返回已經(jīng)授權(quán)的Subject
用戶實(shí)例,同時隨后任意次數(shù)的執(zhí)行subject.isAuthenticated()
方法都將返回true
(真).
但是如果登錄失敗會怎么樣?比如,假如終端用戶提供了一個錯誤的密碼,或者訪問系統(tǒng)次數(shù)太多又或者他們的賬戶被鎖定?
Shiro有一個豐富的運(yùn)行時AuthenticationException
(身份驗(yàn)證異常)層次結(jié)構(gòu),可以準(zhǔn)確指出嘗試失敗的原因.您可以將登錄封裝在try/catch塊中,捕捉任何希望的異常,并相應(yīng)地對它們作出處理.例如:
try {
currentUser.login(token);
} catch ( UnknownAccountException uae ) { // 未知賬戶異常
...
} catch ( IncorrectCredentialsException ice ) { // 密碼錯誤異常
...
} catch ( LockedAccountException lae ) { // 賬戶被鎖定異常
...
} catch ( ExcessiveAttemptsException eae ) { // 過度嘗試異常
...
} ... catch your own ... // 捕捉你自定義的異常
} catch ( AuthenticationException ae ) {
//沒有預(yù)料到的異常錯誤?
}
// 沒問題,向預(yù)期的那樣運(yùn)行...
如果所有已經(jīng)存在的異常類型都不滿足你的需要,你可以自定義新的AuthenticationException
來表示特定的失敗場景.
登錄失敗提示小技巧
雖然代碼可以根據(jù)需要對特定異常作出反應(yīng)并執(zhí)行邏輯,但安全性最佳的方案是僅在發(fā)生故障時向最終用戶顯示一般故障消息,例如“用戶名或密碼不正確”.這確保沒有特定的信息可供黑客使用,黑客可能正在嘗試不同的攻擊方向.
remeberd(已記住)和authenticated(已授權(quán))
就像上面例子中展示的那樣,除了普通登錄外,Shiro還提供了"remember me"(記住我)的概念.現(xiàn)在有必要指出Shiro中remeberd Subject(已記住的用戶)和實(shí)際authenticated Subject(已認(rèn)證的用戶)的細(xì)微不同點(diǎn):
-
已記住: 一個已記住的
Subject
(用戶)不是匿名用戶,而且此用戶有一個已知的身份(比如:subject.getPrincipals()
方法返回的就非空).但是這個身份是基于上一次認(rèn)證留下的session.subject.isRemembered()
方法返回true
的時候就代表一個subject(用戶)被認(rèn)為是已記住的狀態(tài).
-已授權(quán): 一個已授權(quán)的Subject
(用戶)代表用戶在當(dāng)前session成功認(rèn)證的狀態(tài)(比如:login
[登錄]方法在不拋出任何異常的情況下被調(diào)用).只要subject.isAuthenticated()
方法返回true
就可以認(rèn)定當(dāng)前用戶已認(rèn)證.
互斥性
已記住和已認(rèn)證這兩個狀態(tài)只能存在一個,不能同時存在.
為什么要有區(qū)分?
認(rèn)證這個詞有證明的含義.那就是說已經(jīng)得到保證此用戶已經(jīng)證明了他們是他們說的誰誰誰.
當(dāng)一個用戶只記住了之前的應(yīng)用信息,證明的過程早已成為歷史:已記住身份給出了系統(tǒng)一個當(dāng)前用戶可能是誰的提示,但事實(shí)上,沒有任何辦法保證已記住的用戶就是預(yù)期的用戶.一旦用戶已認(rèn)證,他們不屬于已記住的范圍,因?yàn)樗麄兊纳矸菀呀?jīng)在當(dāng)前session中得到確認(rèn).
因此盡管程序大部分情況下仍可以針對記住的身份執(zhí)行用戶特定的邏輯,比如說自定義的視圖,但不要執(zhí)行敏感的操作直到用戶成功執(zhí)行身份認(rèn)證使其身份得到確定.
例如,檢查一個Subject
(用戶)是否可以訪問財(cái)務(wù)信息應(yīng)該取決于isAuthenticated()
方法被認(rèn)證為真,而不是isRemembered()
方法被記住為真,要確保該Subject
(用戶)是期望用戶的同時還要確保用戶通過身份認(rèn)證.
一個例子說明
下面是一個非常常見的場景幫助說明被記住和被認(rèn)證之間差別為何重要。
假設(shè)你使用淘寶進(jìn)行網(wǎng)上購物,你已經(jīng)成功登錄并且在購物籃中添加了一些書籍,但由于你臨時要參加一個會議,匆忙中你忘記退出登錄,當(dāng)會議結(jié)束,回家的時間到了,于是你離開了辦公室.
第二天當(dāng)你回到工作,你意識到你沒有完成你的購買,于是你回到淘寶網(wǎng)站,這時淘寶網(wǎng)站記得你是誰,并通過你的名字向你打招呼,同時給你提供個性化的圖書推薦,對于淘寶網(wǎng)站,subject.isRemembered()
方法將返回true
.
但是當(dāng)你想訪問已買到的寶貝功能完成購買的時候會怎樣呢?雖然淘寶網(wǎng)站'記住'了你(isRemembered()
== true
),但它不能擔(dān)甭樱現(xiàn)在的你就是原來的你(也許是正在使用你計(jì)算機(jī)的同事).
于是在你執(zhí)行像查看用戶私人信息之類的敏感操作之前,淘寶網(wǎng)站會強(qiáng)制你再次登錄以使他們確信你的身份,在你登錄之后,你的身份已經(jīng)被驗(yàn)證,對于淘寶網(wǎng)站來說此時isAuthenticated()
方法將返回true
.
這類情景經(jīng)常發(fā)生,所以Shiro加入了該功能,你可以在你的程序中使用.現(xiàn)在是使用isRemembered()
還是使用 isAuthenticated()
來定制你的視圖和工作流完全取決于你自己,但Shiro將繼續(xù)維護(hù)這項(xiàng)功能以防你可能會需要.
退出登錄
與已認(rèn)證相對的是釋放所有已知的身份信息,當(dāng)Subject
(用戶)與程序不再交互了,你可以調(diào)用subject.logout()
方法刪除所有身份信息.
currentUser.logout(); // 刪除所有認(rèn)證信息,使session失效,通俗的說就是退出登錄
當(dāng)你調(diào)用logout
,任何現(xiàn)存的session
將變?yōu)椴豢捎貌⑶宜械纳矸菪畔G失(如:在web程序中,位于服務(wù)器的記住我的 Cookie信息將同樣被刪除).
當(dāng)一個Subject
(用戶)退出登錄,Subject
(用戶)被重新認(rèn)定為匿名的,對于web程序,如果需要可以重新login
(登錄).
Web 程序需注意
因?yàn)樵?Web 程序中記住身份信息往往使用cookies,而 cookies 只能在 Response 提交時才能被刪除,所以強(qiáng)烈要求在為最終用戶調(diào)用subject.logout()
之后立即將用戶引導(dǎo)到一個新頁面,確保任何與安全相關(guān)的Cookies如期刪除,這是HTTP本身cookies功能的限制而不是Shiro的限制.
認(rèn)證的順序
直到現(xiàn)在,我們只看到如何在程序代碼中驗(yàn)證一個Suject(用戶),現(xiàn)在我們看一下當(dāng)一個身份驗(yàn)證觸發(fā)時Shiro內(nèi)部發(fā)生了什么.
我們?nèi)允褂弥霸诩軜?gòu)章節(jié)里見到過的架構(gòu)圖,僅將左側(cè)跟認(rèn)證相關(guān)的組件高亮,每一個數(shù)字代表認(rèn)證中的一個步驟:
第1步:程序代碼調(diào)用
Subject.login
方法,向AuthenticationToken
(認(rèn)證令牌)實(shí)例的構(gòu)造函數(shù)傳遞用戶的身份和證明信息.
第2步:Subject
實(shí)例,通常是一個DelegatingSubject
(或其子類)通過調(diào)用securityManager.login(token)
將這個令牌轉(zhuǎn)交給程序的SecurityManager
.
第3步:SecurityManager
,類似于“傘”的安全管理組件,得到令牌后通過調(diào)用authenticator.authenticate(token)
方法簡單地將其轉(zhuǎn)交給內(nèi)部的Authenticator
認(rèn)證器實(shí)例.大部分情況下是一個ModularRealmAuthenticator
實(shí)例,ModularRealmAuthenticator
本質(zhì)上為Apache Shiro提供一個PAM(Pluggable_Authentication_Modules 可插拔認(rèn)證模塊)類型的范例.用來支持在驗(yàn)證過程中協(xié)調(diào)一個或多個Realm實(shí)例(在 PAM 術(shù)語中每一個Realm
稱為一個“模塊”,每個realm代表一個獲取安全數(shù)據(jù)源的方式).
第4步:如程序配置了多個Realm
模塊,ModularRealmAuthenticator
實(shí)例將使用其配置的 AuthenticationStrategy
(認(rèn)證策略)開始一個多Realm
身份驗(yàn)證的嘗試.在Realm
被驗(yàn)證調(diào)用的整個過程中,AuthenticationStrategy
(認(rèn)證策略)被調(diào)用來回應(yīng)每個Realm的結(jié)果.我們將稍后討論 AuthenticationStrategies
(認(rèn)證策略).
注意:單 Realm 程序
如果僅有一個 Realm 被配置,它直接被調(diào)用--在單 Realm 程序中不需要AuthenticationStrategy
第5步:每一個配置的 Realm 都被檢驗(yàn)看其是否支持
提交的AuthenticationToken
,如果支持,則該 Realm 的getAuthenticationInfo
方法將隨著令牌的提交而被調(diào)用,getAuthenticationInfo
方法有效地表示該特定Realm
(安全領(lǐng)域)的單一身份驗(yàn)證嘗試,我們將稍后討論Realm
驗(yàn)證行為.
認(rèn)證器
就像之前提到過的,Shiro的SecurityManager
的實(shí)現(xiàn)類默認(rèn)使用一個ModularRealmAuthenticator
(通用安全領(lǐng)域認(rèn)證器)實(shí)例, ModularRealmAuthenticator
(通用安全領(lǐng)域認(rèn)證器)同樣支持單 Realm 和多 Realm
在一個單 Realm 程序中,ModularRealmAuthenticator
(通用安全領(lǐng)域認(rèn)證器)將直接調(diào)用單獨(dú)的Realm
,如果配置有兩個或以上 Realm,將會使用AuthenticationStrategy
(認(rèn)證策略)實(shí)例來協(xié)調(diào)如何進(jìn)行驗(yàn)證,我們將在下面的章節(jié)中討論 AuthenticationStrategy.
如果你希望用自定義的Authenticator
(認(rèn)證器)來配置SecurityManager
,可以在shiro.ini
中這樣自定義:
[main]
...
authenticator = com.foo.bar.CustomAuthenticator
securityManager.authenticator = $authenticator
盡管在實(shí)際操作中,ModularRealmAuthenticator
(通用安全領(lǐng)域認(rèn)證器)已經(jīng)滿足常見需求.
認(rèn)證策略
當(dāng)一個程序中定義了兩個或多個 realm 時,ModularRealmAuthenticator
(通用安全領(lǐng)域認(rèn)證器)使用一個內(nèi)部的AuthenticationStrategy
(認(rèn)證策略)組件來決定一次認(rèn)證是否成功.
例如,如果一個 Realm 驗(yàn)證一個AuthenticationToken
成功,但其他的都失敗了,那這次嘗試是否被認(rèn)為是成功的呢?是不是所有 Realm 驗(yàn)證都成功了才認(rèn)為是成功?又或者一個 Realm 驗(yàn)證成功后,是否還有必要討論其他Realm? AuthenticationStrategy
(認(rèn)證策略)負(fù)責(zé)幫助解決這些問題.
認(rèn)證策略是一個無狀態(tài)的組件,在身份驗(yàn)證嘗試期間會被查詢4次(這4次交互所需的任何必要狀態(tài)都將作為方法參數(shù)給出):
1.在任何 Realms 被驗(yàn)證之前
2.在某個的 Realm 的 getAuthenticationInfo 方法調(diào)用之前
3.在某個的 Realm 的 getAuthenticationInfo 方法調(diào)用之后
4.在所有的 Realm 被驗(yàn)證之后
AuthenticationStrategy
還有責(zé)任從每一個成功的 Realm 中收集結(jié)果并將它們綁定到一個單獨(dú)的 AuthenticationInfo
,這個AuthenticationInfo
實(shí)例是被 Authenticator 實(shí)例返回的,并且 Shiro 用它來展現(xiàn)一個 Subject
(用戶)的最終身份(也就是 Principals).
Subject(用戶)身份view(展示)
如果你在程序中使用多于一個的 Realm 從多個數(shù)據(jù)源中獲取帳戶數(shù)據(jù),AuthenticationStrategy
最終負(fù)責(zé)應(yīng)用程序所看到的 Subject 身份最終合并的視圖.
Shiro 有3個具體的
AuthenticationStrategy
實(shí)現(xiàn)類:
實(shí)現(xiàn)類 : 描述
-
AtLeastOneSuccessfulStrategy
: 如果有一個或多個Realm驗(yàn)證成功,整體認(rèn)證被認(rèn)為是成功的,如果沒有一個驗(yàn)證成功,則該次認(rèn)證失敗 -
FirstSuccessfulStrategy
: 只有從第一個成功驗(yàn)證的Realm返回的信息會被使用,以后的Realm將被忽略,如果沒有一個驗(yàn)證成功,則該次認(rèn)證失敗 -
AllSuccessfulStrategy
: 所有配置的Realm在全部嘗試中都成功驗(yàn)證才被認(rèn)為是成功,如果有一個驗(yàn)證不成功,則該次認(rèn)證失敗
ModularRealmAuthenticator
默認(rèn)使用AtLeastOneSuccessfulStrategy
實(shí)現(xiàn),這也是最常用的策略,然而你也可以配置成你希望的其他策略:
[main]
...
authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authcStrategy
...
自定義的認(rèn)證策略
如果你希望創(chuàng)建你自己的AuthenticationStrategy
實(shí)現(xiàn),你可以使用 org.apache.shiro.authc.pam.AbstractAuthenticationStrategy
作為起點(diǎn).AbstractAuthenticationStrategy
類自動實(shí)現(xiàn)綁定/聚集行為同時將來自于每一個 Realm 的結(jié)果收集到一個 AuthenticationInfo
實(shí)例中.
Realm 驗(yàn)證的順序
非常重要的一點(diǎn)是,和 Realm 交互的ModularRealmAuthenticator
按迭代(iteration)順序執(zhí)行
ModularRealmAuthenticator
可以訪問為SecurityManager
配置的Realm
實(shí)例,當(dāng)嘗試一次認(rèn)證時,它將在集合中遍歷,支持對提交的AuthenticationToken
進(jìn)行處理的每個Realm
都將執(zhí)行 Realm 的 getAuthenticationInfo
方法
隱含的順序
在使用 Shiro INI 配置文件時,你可以通過配置 Realm 來使程序按你希望的順序去處理 AuthenticationToken,例如,在shiro.ini
中,Realm 將按照他們在 INI 文件中定義的順序執(zhí)行:
blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm
SecurityManager上配置了這三個 Realm,但沒有設(shè)置securityManager.realms屬性,此時在一個驗(yàn)證過程中blahRealm, fooRealm, 和 barRealm 將被按照他們定義的先后順序執(zhí)行.
加不加下面的配置順序都一樣:
securityManager.realms = $blahRealm, $fooRealm, $barRealm
使用這種配置方式,你不需要調(diào)用方法來設(shè)置
securityManager
的 realms 屬性,在程序讀取INI文件時每一個被定義的realm 將自動分配到 realms 屬性中
指定的順序
如果你希望明確定義 realm 執(zhí)行的順序,不管他們?nèi)绾伪欢x,你可以設(shè)置 SecurityManager
的 realms 屬性,例如,使用上面定義的 realm ,但你希望 blahRealm 最后執(zhí)行而不是第一個:
blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm
securityManager.realms = $fooRealm, $barRealm, $blahRealm
...
明確包含Realm
當(dāng)你明確的配置 securityManager.realms
屬性時, 只有 被引用的 realm 將為 SecurityManager
配置,也就是說你可能在 INI 中定義了5個 realm , 但實(shí)際上只使用了3個, 如果在 realms 屬性中只引用了3個, 這和隱含的 realm 順序不同, 在那種情況下, 所有有效的 realm 都會用到.
Realm認(rèn)證
本章闡述了當(dāng)嘗試一次認(rèn)證時 Shiro 主要的工作流程,而在驗(yàn)證過程中,用到的 Realm 內(nèi)產(chǎn)生的工作流程(如上面提到的第5步)將在 Realm 章中 Realm Authentication 節(jié)討論