Apache Shiro 驗證
驗證(Authentication)
驗證(Authentication):身份驗證的過程--也就是證明一個用戶的真實身份。為了證明用戶身份,需要提供系統理解和相信的身份信息和證據。
需要通過向shiro提供用戶的身份(Principals)和證明(credentials)來判定是否和系統所要求的匹配。
身份(Principals)是Subject的“身份屬性”,可以是任何與Subject相關的標識,比如說名稱(給定名稱)、名字(姓或者昵稱)、用戶名、安全號碼等等,當然像昵稱這樣的內容不能很好的對Subject進行獨特標識,所以最好的身份信息(Principals)是使用在程序中唯一的標識--典型的使用用戶名或郵件地址。
最主要的身份
雖然shiro可以使用任何數量的身份,Shiro還是希望一個程序精確地使用一個主要的身份--一個僅有的唯一標識Subject值。在多數程序中經常會是一個用戶名、郵件地址或者全局唯一的用戶ID。
證明(Credentials)通常是只有Subject知道的機密內容,用來證明他們真正擁有所需的身份,一些簡單的證書例子如密碼、指紋、眼底掃描和X.509證書等。
最常見的身份/證明是用戶名和密碼,用戶名是所需的身份說明,密碼是證明身份的證據。如果一個提交的密碼和系統要求的一致,程序就認為該用戶身份正確因為其他人不應該知道同樣的密碼。
Authenticating對象
Subject驗證的過程可以有效地劃分分以下三個步驟:
1.收集Subject提交的身份和證明;
2.向Authenticating提交身份和證明;
3.如果提交的內容正確,允許訪問,否則重新嘗試驗證或阻止訪問。
下面的代碼示范了Shiro API如何實現這些步驟:
第一步:收集用戶身份和證明
//Example using mostcommon scenario of username/password pair:
UsernamePasswordTokentoken = new UsernamePasswordToken(username, password);
//”Remember Me” built-in:
token.setRememberMe(true);
在這里我們使用UsernamePasswordToken,支持所有常用的用戶名/密碼驗證途徑,這是一個org.apache.shiro.authc.AuthenticationToken接口的實現,這個接口被shiro認證系統用來提交身份和證明。
注意shiro并不關心你如何獲取這些住處:也許是用戶從一個HTML表單中提交的,或者可能從一個HTTP請求字串中解析的,也可能來自于Swing或者Flex GUI的密碼表單,或者通過命令行參數得到。從程序終端用戶獲取信息的過程與shiro的AuthenticationToken完全無關。
你可以隨自己喜歡構造和引用AuthenticationToken--它是協議不可知論者。
這個例子同樣顯示我們希望shiro在嘗試驗證時執行“Remember Me”服務,這確保shiro在用戶今后返回系統時能記住他們的身份,我們會在以后的章節討論“Remember Me”服務。
第二步:提交身份和證明
當身份和證明住處被收集并實例化為一個認證令牌后,我們需要向shiro提交令牌以執行真正的驗證嘗試:
Subject currentUser =SecurityUtils.getSubject();
currentUser.login(token);
在獲取當前執行的Subject后,我們執行一個單獨的login命令,將之前創建的認證令牌實例傳給它。
使用login方法將有效地執行身份驗證。
第三步:處理成功或失敗
當login函數沒有返回信息時表明驗證通過了。程序可以繼續運行,此時執行SecurityUtils.getSubject()將返回驗證后的Subject實例,subject.isAuthenticated()將返回true。
但是如果login失敗了呢?例如,用戶提供了一個錯誤的密碼或者因訪問系統次數過多而被鎖定將會怎樣呢?
shiro擁有豐富的運行期異常(AuthenticationException)可以精確標明為何驗證失敗,你可以將login放入到try/catch塊中并捕獲所有你想捕獲的異常并對它們做出處理。例如:
try {
currentUser.login(token);
} catch (UnknownAccountException uae ) { ...
} catch (IncorrectCredentialsException ice ) { ...
} catch (LockedAccountException lae ) { ...
} catch (ExcessiveAttemptsException eae ) { ...
} ... catch your own...
} catch (AuthenticationException ae ) {
//unexpected error?
}
//No problems,continue on as expected...
如果原有的異常不能滿足你的需求,可以創建自定義的AuthenticationExceptions來表示特定的失敗場景。
登錄失敗小貼士
雖然你的代碼可以對指定的異常做出處理并執行某些所需的邏輯,但有經驗的安全做法是僅向終端用戶輸出一般的失敗信息,例如“錯誤的用戶名和密碼”。這確保不向嘗試攻擊你的黑客提供有用的信息。
已記住(Remembered) vs 已驗證(Authenticated)
如上例所示,shiro支持在登錄過程中執行"remember me",在此值得指出,一個已記住的Subject(remembered Subject)和一個正常通過認證的Subject(authenticated Subject)在shiro是完全不同的。
記住的(Remembered):一個被記住的Subject沒有已知身份(也就是說subject.getPrincipals()返回空),但是它的身份被先前的認證過程記住,并存于先前session中,一個被認為記住的對象在執行subject.isRemembered()返回真。
已驗證(Authenticated):一個被驗證的Subject是成功驗證后(如登錄成功)并存于當前session中,一個被認為驗證過的對象調用subject.isAuthenticated()將返回真。
互斥的
已記住(Remembered)和已驗證(Authenticated)是互斥的--一個標識值為真另一個就為假,反過來也一樣。
為什么區分?
單詞驗證(authentication)有明顯的證明含義,也就是說,需要擔保Subject已經被證明他們是誰。
當一個用戶僅僅在上一次與程序交互時被記住,證明的狀態已經不存在了:被記住的身份只是給系統一個信息這個用戶可能是誰,但不確定,沒有辦法擔保這個被記住的Subject是所要求的用戶,一旦這個subject被驗證通過,他們將不再被認為是記住的因為他們的身份已經被驗證并存于當前的session中。
所以盡管程序大部分情況下仍可以針對記住的身份執行用戶特定的邏輯,比如說自定義的視圖,但不要執行敏感的操作直到用戶成功執行身份驗證使其身份得到確定。
例如,檢查一個Subject是否可以訪問金融信息應該取決于是否被驗證(isAuthenticated())而不是被記住(isRemembered()),要確保該Subject是所需的和通過身份驗證的。
一個例子說明
下面是一個非常常見的場景幫助說明被記住和被驗證之間差別為何重要。
假設你使用卓越網,你已經成功登錄并且在購物藍中添加了一些書籍,但你由于臨時要參加一個會議,匆忙中你忘記退出登錄,當會議結束,回家的時間到了,于是你離開了辦公室。
第二天當你回到工作,你意識到你沒有完成你的購買動作,于是你回到卓越網,這時,卓越網“記得”你是認證,通過你的名字向你打招呼,仍舊給你提供個性化的圖書推薦,對于卓越,subject.isRemembered()將返回真。
但是當你想訪問你帳號的信用卡信息完成圖書購買的時候會怎樣呢?雖然卓越“記住”了你(isRemembered() == true),它不能擔保你就是你(也許是正在使用你計算機的同事)。
于是,在你執行像使用信用卡信息之類的敏感操作之前,卓越強制你登錄以使他們擔保你的身份,在你登錄之后,你的身份已經被驗證,對于卓越,isAuthenticated()將返回真。
這類情景經常發生,所以shiro加入了該功能,你可以在你的程序中使用。現在是使用isRemembered()還是使用isAuthenticated()來定制你的視圖和工作流完全取決于你自己,但shiro維護這種狀態基礎以防你可能會需要。
退出登錄
與驗證相對的是釋放所有已知的身份信息,當Subject與程序不再交互了,你可以調用subject.logout()丟掉所有身份信息。
currentUser.logout();//removes all identifying information and invalidates their session too.
當你調用logout,任何現存的session將變為不可用并且所有的身份信息將消失(如:在web程序中,RememberMe的Cookie信息同樣被刪除)。
當一個Subject退出登錄,Subject被重新認定為匿名的,對于web程序,如果需要可以重新登錄。
Web程序需注意
因為在Web程序中記住身份信息往往使用Cookies,而Cookies只能在Response提交時才能被刪除,所以強烈要求在為最終用戶調用subject.logout()之后立即將用戶引導到一個新頁面,確保任何與安全相關的Cookies如期刪除,這是Http本身Cookies功能的限制而不是Shiro的限制。
認證序列
直到現在,我們只看到如何在程序代碼中驗證一個Suject,現在我們看一下當一個驗證發生時shiro內部發生了什么。
我們仍使用之前在架構(architecture)章節里見到過的架構圖,僅將左側跟認證相關的組件高亮,每一個數字代表認證中的一個步驟:
第1步:程序代碼調用Subject.login方法,向AuthenticationToken(公證處)實例的構造方法傳遞最終用戶的身份和證明。
第2步:Subject實例,通常是一個 DelegatingSubject(或其子類)通過調用securityManager.login(token)將這個令牌轉交給程序的SecurityManager。
第3步:SecurityManager,基本的“安全傘”組件,得到令牌并通過調用authenticator.authenticate(token)簡單地將其轉交它內部的Authenticator實例,大部分情況下是一個ModularRealmAuthenticator實例,用來支持在認證過程中協調一個或多個Realm實例。ModularRealmAuthenticator本質上為Apache Shiro(在PAM術語中每一個Realm稱為一個“模塊”)提供一個PAM類型的范例。
第4步:如程序配置了多個Realm,ModularRealmAuthenticator實例將使用其配置的 AuthenticationStrategy開始一個多Realm身份驗證的嘗試。在此之前,之中及之后Realm被驗證調用,AuthenticationStrategy被調用以允許其同每一個Realm結果交互,我們將稍后討論AuthenticationStrategies。
注意:單Realm程序
如果僅有一個Realm被配置,它直接被調用--在單Realm程序中不需要AuthenticationStrategy。
第5步:每一個配置的Realm都被檢驗看其是否支持提交的AuthenticationToken,如果支持,則該Realm的 getAuthenticationInfo方法將使用提交的牌作調用,getAuthenticationInfo方法為特定的Realm有效提供一次獨立的驗證嘗試,我們將簡短地討論Realm驗證行為。
Authenticator
就像以前提到過的,Shiro SecurityManager implementations默認使用一個 ModularRealmAuthenticator實例, ModularRealmAuthenticator同樣支持單Realm和多Realm。
在一個單Realm程序中,ModularRealmAuthenticator將直接執行單獨的Realm,如果配置有兩個或以上Realm,將會使用AuthenticationStrategy實例來協調如何進行驗證,我們將在下面討論AuthenticationStrategy。
如果你希望用自定義的Authenticator實現配置SecurityManager,你可以在shiro.ini中做這件事,如:
[main]
...
authenticator =com.foo.bar.CustomAuthenticator
securityManager.authenticator= $authenticator
盡管在實際操作中, ModularRealmAuthenticator適用于大部分需求。
AuthenticationStrategy
當一個程序中定義了兩個或多個realm時,ModularRealmAuthenticator信任一個內部的AuthenticationStrategy組件來決定一個驗證是否成功的條件。
例如,如果一個Realm驗證一個AuthenticationToken成功,但其他的都失敗了,那這次嘗試是否被認為是成功的呢?不是所有Realm驗證都成功了才認為是成功?又或者一個Realm驗證成功,是否還有必要討論其他Realm?AuthenticationStrategy根據程序需求做出恰當的決定。
AuthenticationStrategy是一個stateless的組件,在整個驗證過程中在以下四個時候會被用到(當在這四個活動中需要必要的state時,state將作為方法參數傳遞)
1.在任何Realms被執行之前;
2.緊接在單獨的Realm的getAuthenticationInfo方法調用之前;
3.緊接著單獨的Realm的getAuthenticationInfo方法調用之后;
4.在所有的Realm被執行之后。
AuthenticationStrategy還有責任從每一個成功的Realm中收集結果并將它們“綁定”到一個單獨的AuthenticationInfo,這個AuthenticationInfo實例是被Authenticator實例返回的,并且shiro用它來展現一個Subject的最終身份(也就是Principals)。
Subject身份“展示(view)”
如果你在程序中使用多于一個的Realm從多個數據源中獲取帳戶數據,程序可看到的Subject身份最終“融合(merged)”的展示是AuthenticationStrategy最終的責任。
shiro有3個具體的AuthenticationStrategy實現:
AtLeastOneSuccessfulStrategy:如果一個或多個Realm驗證成功,所有的嘗試都被認為是成功的,如果沒有一個驗證成功,則該次嘗試失敗;
FirstSuccessfulStrategy:只有從第一個成功驗證的Realm返回的信息會被使用,以后的Realm將被忽略,如果沒有一個驗證成功,則該次嘗試失敗。
SuccessfulStrategy:所有配置的Realm在全部嘗試中都成功驗證才被認為是成功,如果有一個驗證不成功,則該次嘗試失敗。
ModularRealmAuthenticator默認使用AtLeastOneSuccessfulStrategy實現,這也是最常用的策略,然而你也可以配置你希望的不同的策略。
shiro.ini
[main]
...
authcStrategy =org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy= $authcStrategy
...
自定義的AuthenticationStrategy
如果你希望創建你自己的AuthenticationStrategy
實現,你可以使用org.apache.shiro.authc.pam.AbstractAuthenticationStrategy作為起始點。AbstractAuthenticationStrategy類自動實現 '綁定(bundling)'/聚集(aggregation)行為將自于每個Realm的結果收集到一個AuthenticationInfo實例中。
Realm驗證的順序
非常重要的一點是,將和Realm交互的ModularRealmAuthenticator按iteration順序執行。
ModularRealmAuthenticator可以訪問為SecurityManager配置的Realm實例,當嘗試一次驗證時,它將在集合中遍歷,支持對提交的AuthenticationToken處理的每個Realm都將執行Realm的getAuthenticationInfo方法。
隱含的順序
在使用ShiroINI配置文件形式時,你可以按你希望其處理AuthenticationToken的順序來配置Realm,例如,在shiro.ni中,Realm將按照他們在INI文件中定義的順序執行。
blahRealm =com.company.blah.Realm
...
fooRealm =com.company.foo.Realm
...
barRealm =com.company.another.Realm
SecurityManager上配置了這三個Realm,在一個驗證過程中,blahRealm, fooRealm, 和 barRealm將被順序執行。
這基本上與定義下面這一行語句的效果相同:
securityManager.realms= $blahRealm, $fooRealm, $barRealm
使用這種方法,你不需要設置securityManager的realm懺悔,每一個被定義的realm將自動加入到realms屬性中。
明確的順序
如果你希望明確定義realm執行的順序,不管他們如何被定義,你可以設置SecurityManager的realms屬性,例如,使用上面定義的realm,但你希望blahRealm最后執行而不是第一個:
blahRealm =com.company.blah.Realm
...
fooRealm =com.company.foo.Realm
...
barRealm =com.company.another.Realm
securityManager.realms= $fooRealm, $barRealm, $blahRealm
...
明確Realm包含
當你明確的配置securityManager.realms屬性時,只有被引用的realm將為SecurityManager配置,也就是說你可能在INI中定義了5個realm,但實際上只使用了3個,如果在realm屬性中只引用了3個,這和隱含的realm順序不同,在那種情況下,所有有效的realm都會用到。
Realm驗證
本章闡述了當一個驗證嘗試發生時shiro主要的工作流程,而在驗證過程中,用到的realm內產生的工作流程(如上面提到的第5步)將在Realm章中Realm Authentication節討論。
為文檔加把手
我們希望這篇文檔可以幫助你使用Apache Shiro進行工作,社區一直在不斷地完善和擴展文檔,如果你希望幫助shiro項目,請在你認為需要的地方考慮更正、擴展或添加文檔,你提供的任何點滴幫助都將擴充社區并且提升Shiro。
提供你的文檔的最簡單的途徑是將它發送到用戶論壇(http://shiro-user.582556.n2.nabble.com/)或郵件列表(http://shiro.apache.org/mailing-lists.html)