???????本文基于shiro 認證框架分析登錄邏輯中是如何進行用戶識別認證的,文章會涉及到shiro 的CacheManager、SessionManager、多Realm的Strategy認證策略等的配置,相信會讓剛接觸到shiro框架的老哥們能快速上手去配置自定義的一些內容。
1、開始執(zhí)行登錄,由Subject提交登錄請求
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginVO.getLoginName(), loginVO.getPassword());
if(!subject.isAuthenticated()){
try{
subject.login(token);
doSth...
}catch (Exception e){
doSth...
}
}
2、subject中轉叫給對應的securityManager執(zhí)行登錄

3、securityManager利用本身的 Authenticator 認證器進行認證。而Authenticator對象本身是一個接口,它使用的是主要抽象類AbstractAuthenticator中的 authenticate(token) 方法,然后這個方法中再調用抽象方法的doAuthenticate(token)去實現認證。實際的認證邏輯就是轉交到了它們的實現類中去寫認證邏輯,可以根據這一點自定義實現類改變一些認證原理。
???????在我們沒有自定義去實現Authenticator的情況下,系統(tǒng)自動調用了認證器默認的實現類ModularRealmAuthenticator去處理這個邏輯。接下來還是回去跟蹤這個默認實現類去理解。

4、接下來是跟蹤上面默認實現類的doAuthenticate認證方法,代碼如下。

???????這個方法很重要,它決定了我們將要如何去使用我們定義的Realm數據源去匹配數據,以及多Realm的情況下,如何配置認證策略。
???????代碼第二句是獲取所有的Realm。這個Realm是我們自定義設置的,看方法邏輯是從本身的Authenticator 對象中的一個屬性獲取內容。通常情況下,我實現單Realm的認證,可以直接使用securityManager.setRealm( Realm realm)方法,它的內部邏輯幫助我們將realm塞到了當前Authenticator 對象中。但是在多Realm數據源的情況下,上述方法顯然不滿足我們集合的需求。此時我們自己去修改ModularRealmAuthenticator,示例如下(最后需要將該authenticator設置到SecurityManager中)
@Bean
protected Authenticator authenticator(){
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
FirstSuccessfulStrategy strategy = new FirstSuccessfulStrategy();
HashSet<Realm> realmSet = new HashSet<Realm>();
realmSet.add(realm());
// 同時設置相關的realm 和 strategy
authenticator.setRealms(realmSet);
authenticator.setAuthenticationStrategy(strategy);
return authenticator;
}
???????上述代碼中提到的策略是說在多Realm環(huán)境下的認證邏輯限制,包含以下三種情況。用戶可自定義選擇某一種使用
FirstSuccessfulStrategy 只要有一個成功即可,返回第一個成功的信息
AtLeatOneSuccessfulStrategy 只要有一個成功即可,返回所有成功了的信息
AllSuccessfulStrategy 所有Realm驗證成功才算成功,且返回所有Realm身份認證成功的認證信息
5、假設目前的環(huán)境是單Realm的情況。用戶在在定義Realm的時候,一般實現的接口都是AuthenticatingRealm。
???????實現驗證的過程也是需要從Realm的doGetAuthenticationInfo()方法中獲取 AuthoricationInfo對象。登錄的驗證實際山就是AuthoricationInfo和用戶傳入的Token對象進行對比驗證。
???????具體對比是使用this.assertCredentialsMatch(token, info); 這個調用的方法去實現的。如果在對比過程中發(fā)現一些不一致的內容,它就會自動拋出一些異常。
???????順帶一提,本人在一些shiro教程的博客里面,看到博主在自定義Realm的時候就對一些password作對比判斷,個人覺得這樣做有點偏離了初衷,但是實際上效果也會差不多,只不過需要用戶在密碼等內容不正確的時候返回null即可。

6、從圖4中可以看出來,我們在使用this.doGetAuthencationInfo(token)獲取AuthencationInfo認證信息之前,有一步從緩存獲取的操作,這個操作很重要。我們可不能每次都要求shiro去數據庫查詢用戶認證信息,這給予數據庫的壓力太大了。因此在研究一下Cache緩存內容。
???????很容易能夠看出來,我們要實現緩存首先需要實現一個接口 org.apache.shiro.cache.Cache<K, V>接口即可。Cache的植入有兩種方式:(A 的優(yōu)先級高于 B 的)
A) 在之前的自定義Realm中再通過重寫setAuthenticationCache()方法將Cache<K, V>注入到Realm中即可。
B)當然還有另外一種方式獲取Cache,在Realm本身是沒有Cache對象的時候,可以默認從CacheManager對象中獲取一個Cache對象。因此我們還可以自定義實現一個CacheManager接口的類,這個類需要實現一個getCache的方法,返回的也是Cache<K, V>。 在實現了CacheManager將它注入到SecurityManager中即可。


7、認證成功以后,返回AuthenticationInfo對象一直回到SecurityManager中。并利用傳入的token,以及info等,利用SubjectContext上下文存儲的內容構建最新的Subject,將subject的登錄狀態(tài)改為 true,并將info和token都存儲進去。最后返回最后更新了狀態(tài)的Subject

8、在然后有一個onSuccessfulLogin(token, info, loggedIn) 方法。它主要是處理RememberMe相關的內容。首先獲取SecurityManager中封裝的屬性RememberMeManager。這個對象執(zhí)行onSuccessfulLogin()方法的邏輯,會先forgetIdentity(subject)清除掉原本有的信息,然后判斷token中是否有rememberMe屬性的存在,或是這個屬性應該是true值。因此在我們在登陸時候,應該封裝Token的時候給rememberMe這個屬性賦值。
???????最后再執(zhí)行 this.rememberIdentity(subject, token, info) 這個方法儲存登陸信息

9、自定義繼承AbstractRememberMeManager類需要實現四個方法。而Shiro已經為我們提供了一個可以直接使用的CookieRememberMeManager的實現類,我們可以直接將它注入到SecurityManager中。
10、之后返回到了DelegatingSubject類,驗證了返回Subject之后繼續(xù)后面的邏輯。
PrincipalCollection對象應該是封裝到Subject中的通過不同策略的返回的身份集合
Session session = subject.getSession(false);

???????Subject對象的Session屬性本身是在SecurityUtils.getSubject(); 方法創(chuàng)建 Subject對象的時候就已經從SessionManager中創(chuàng)建了Session對象并封裝。(如果之前本身沒有Session,可以通過getSession() 方法,它里面會自動使用SessionManager 創(chuàng)建一個Session出來。因此SessionManager 也是可以自定義實現的)
11、SessionManager 會話管理

AbstractSessionDAO提供了SessionDAO的基礎實現,如生成會話ID等;CachingSessionDAO提供了對開發(fā)者透明的會話緩存的功能,只需要設置相應的CacheManager即可;MemorySessionDAO直接在內存中進行會話維護;EnterpriseCacheSessionDAO提供了緩存功能的會話維護,默認情況下使用MapCache實現,內部使用ConcurrentHashMap保存緩存的會話。
Shiro會話管理SessionManager
SecurityUtils.getSubject() 方法創(chuàng)建Subject對象時,會先創(chuàng)建一個SubjectContext 對象,然后將SecurityManager Session(利用SessionManager獲取) 和PrincipalCollection 三個對象封裝進了SubjectContext ,再使用工廠類getSubjectFactory創(chuàng)建出subject 對象,即shiro中的Client端。

首先SecurityManager是需要我們自己定義的,定義成Bean后自動裝載;
Session:Session 則是從SubjectContext 中獲取或者利用SubjectContext 中的sessionId 從SessionManager中獲取已有的。第一次創(chuàng)建自然是兩個地方都不會存在,因此此時session還是為null
PrincipalCollection 則是先按照順序依次從SubjectContext、Subject、Session和RememberMe中獲取,因為是第一次創(chuàng)建,這些內容還沒有,還是為null
下面的 save 方法,則是調用DefaultSecurityManager 中 subjectDAO (默認使用實現類DafaultSessionDAO)保存會話。主要是想要將已經儲存的用戶信息和認證信息存儲到會話中。