今天想和大家聊一聊 Shiro 中的多 Realm 認證策略問題~
在項目中,如果我們想手機驗證碼登錄、第三方 QQ 登錄、郵箱登錄等多種登錄方式共存,那麼就可以考慮通過 Shiro 中的多 Realm 來實現,具體操作中,一個 Realm 剛好就對應一種登錄方式。
多 Realm 登錄的用法并不難,松哥之前也專門發過相關的文章和大家分享,傳送門:
今天我不想聊用法,主要是想和大家聊一聊這裡相關的源碼。因此本文需要大家有一定的 Shiro 使用經驗,若無,可以參考上面的鍊接惡補一下。
1. ModularRealmAuthenticator1.1 Realm 去哪了?我們配置的 Realm,可以直接配置給 SecurityManager,也可以配置給 SecurityManager 中的 ModularRealmAuthenticator。
如果我們是直接配置給 SecurityManager,那麼在完成 Realm 的配置後,會自動調用 afterRealmsSet 方法,在該方法的中,會将我們配置的所有 Realm 最終配置給 ModularRealmAuthenticator。
相關源碼如下:
RealmSecurityManager#setRealm(RealmSecurityManager 是 DefaultWebSecurityManager 的父類)
public void setRealm(Realm realm) {
if (realm == null) {
throw new IllegalArgumentException("Realm argument cannot be null");
}
Collection<Realm> realms = new ArrayList<Realm>(1);
realms.add(realm);
setRealms(realms);
}
public void setRealms(Collection<Realm> realms) {
if (realms == null) {
throw new IllegalArgumentException("Realms collection argument cannot be null.");
}
if (realms.isEmpty()) {
throw new IllegalArgumentException("Realms collection argument cannot be empty.");
}
this.realms = realms;
afterRealmsSet();
}
可以看到,無論是設置單個 Realm 還是設置多個 Realm,最終都會調用到 afterRealmsSet 方法,該方法在 AuthorizingSecurityManager#afterRealmsSet 類中被重寫,内容如下:
protected void afterRealmsSet() {
super.afterRealmsSet();
if (this.authorizer instanceof ModularRealmAuthorizer) {
((ModularRealmAuthorizer) this.authorizer).setRealms(getRealms());
}
}
可以看到,所有的 Realm 最終都被設置給 ModularRealmAuthenticator 了。
所以說,無論是單個 Realm 還是多個 Realm,最終都是由 ModularRealmAuthenticator 統一管理統一調用的。
1.2 ModularRealmAuthenticator 怎麼玩ModularRealmAuthenticator 中核心的方法就是 doAuthenticate,如下:
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);
}
}
這個方法的邏輯很簡單:
配置一個 Realm 的情況比較簡單,不在本文的讨論範圍内,本文主要是想和大家讨論多個 Realm 的情況。
當存在多個 Realm 的時候,必然又會帶來另一個問題:認證策略,即怎麼樣就算認證成功?一個 Realm 認證成功就算成功還是所有 Realm 認證成功才算成功?還是怎麼樣。
接下來我們來詳細聊一聊這個話題。
2. AuthenticationStrategy先來整體上看下,負責認證策略的類是 AuthenticationStrategy,這是一個接口,有三個實現類:
單從字面上來看,三個實現類都好理解:
第二種其實很好理解,問題在于第 1 個和第 3 個,這兩個單獨理解也好理解,放在一起的話,那有人不禁要問,這倆有啥區别?
老實說,在 1.3.2 之前的版本還真沒啥大的區别,不過現在最新版本還是有些區别,且聽松哥來分析。
首先這裡一共涉及到四個方法:
第一個和第四個方法在每次認證流程中隻調用一次,而中間兩個方法則在每個 Realm 調用前後都會被調用到,僞代碼就類似下面這樣:
上面這四個方法,在 AuthenticationStrategy 的四個實現類中有不同的實現,我整理了下面一張表格,方便大家理解:
大家注意這裡多了一個 merge 方法,這個方法是在 AbstractAuthenticationStrategy 類中定義的,當存在多個 Realm 時,合并多個 Realm 中的認證數據使用的。接下來我們就按照這張表的順序,來挨個分析這裡的幾個方法。
2.1 AbstractAuthenticationStrategy2.1.1 beforeAllAttempts直接來看代碼吧:
public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
return new SimpleAuthenticationInfo();
}
這裡啥都沒幹,就創建了一個空的 SimpleAuthenticationInfo 對象。
2.1.2 beforeAttempt
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
return aggregate;
}
這個方法的邏輯也很簡單,傳入的 aggregate 參數是指多個 Realm 認證後聚合的結果,這裡啥都沒做,直接把結果原封不動返回。
2.1.3 afterAttempt
public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException {
AuthenticationInfo info;
if (singleRealmInfo == null) {
info = aggregateInfo;
} else {
if (aggregateInfo == null) {
info = singleRealmInfo;
} else {
info = merge(singleRealmInfo, aggregateInfo);
}
}
return info;
}
這是每個 Realm 認證完成後要做的事情,參數 singleRealmInfo 表示單個 Realm 認證的結果,aggregateInfo 表示多個 Realm 認證結果的聚合,具體邏輯如下:
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
return aggregate;
}
這裡直接把聚合結果返回,沒啥好說的。
2.1.5 merge
protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
if( aggregate instanceof MergableAuthenticationInfo ) {
((MergableAuthenticationInfo)aggregate).merge(info);
return aggregate;
} else {
throw new IllegalArgumentException( "Attempt to merge authentication info from multiple realms, but aggregate "
"AuthenticationInfo is not of type MergableAuthenticationInfo." );
}
}
merge 其實就是調用 aggregate 的 merge 方法進行合并,正常情況下我們使用的 SimpleAuthenticationInfo 就是 MergableAuthenticationInfo 的子類,所以這裡合并沒問題。
2.2 AtLeastOneSuccessfulStrategy2.2.1 beforeAllAttempts同 2.1.1 小節。
2.2.2 beforeAttempt同 2.1.2 小節。
2.2.3 afterAttempt同 2.1.3 小節。
2.2.4 afterAllAttempts
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
//we know if one or more were able to successfully authenticate if the aggregated account object does not
//contain null or empty data:
if (aggregate == null || isEmpty(aggregate.getPrincipals())) {
throw new AuthenticationException("Authentication token of type [" token.getClass() "] "
"could not be authenticated by any configured realms. Please ensure that at least one realm can "
"authenticate these tokens.");
}
return aggregate;
}
這裡的邏輯很明确,就是當聚合結果為空就直接抛出異常。
2.2.5 merge同 2.1.5 小節。
2.2.6 小結結合 2.1 小節的内容,我們來梳理一下 AtLeastOneSuccessfulStrategy 的功能。
這就是 AtLeastOneSuccessfulStrategy 的認證策略。可以看到:如果隻有一個 Realm 認證成功,那麼正常返回,如果有多個 Realm 認證成功,那麼返回的用戶信息中将包含多個認證用戶信息。
可以通過如下方式獲取返回的多個用戶信息:
Subject subject = SecurityUtils.getSubject();
subject.login(token);
PrincipalCollection principals = subject.getPrincipals();
List list = principals.asList();
for (Object o : list) {
System.out.println("o = " o);
}
subject.getPrincipals() 方法可以獲取多個認證成功的憑證。
2.3 AllSuccessfulStrategy2.3.1 beforeAllAttempts同 2.1.1 小節。
2.3.2 beforeAttempt
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
if (!realm.supports(token)) {
String msg = "Realm [" realm "] of type [" realm.getClass().getName() "] does not support "
" the submitted AuthenticationToken [" token "]. The [" getClass().getName()
"] implementation requires all configured realm(s) to support and be able to process the submitted "
"AuthenticationToken.";
throw new UnsupportedTokenException(msg);
}
return info;
}
可以看到,這裡就是去檢查了下 Realm 是否支持當前 token。
這塊的代碼我覺得略奇怪,為啥其他認證策略都不檢查,隻有這裡檢查?感覺像是一個 BUG。有懂行的小夥伴可以留言讨論下這個問題。
2.3.3 afterAttempt
public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info, AuthenticationInfo aggregate, Throwable t)
throws AuthenticationException {
if (t != null) {
if (t instanceof AuthenticationException) {
throw ((AuthenticationException) t);
} else {
String msg = "Unable to acquire account data from realm [" realm "]. The ["
getClass().getName() " implementation requires all configured realm(s) to operate successfully "
"for a successful authentication.";
throw new AuthenticationException(msg, t);
}
}
if (info == null) {
String msg = "Realm [" realm "] could not find any associated account data for the submitted "
"AuthenticationToken [" token "]. The [" getClass().getName() "] implementation requires "
"all configured realm(s) to acquire valid account data for a submitted token during the "
"log-in process.";
throw new UnknownAccountException(msg);
}
merge(info, aggregate);
return aggregate;
}
如果當前認證出錯了,或者認證結果為 null,就直接抛出異常(因為這裡要求每個 Realm 都認證成功,但凡有一個認證失敗了,後面的就沒有必要認證了)。
如果一切都 OK,就會結果合并然後返回。
2.3.4 afterAllAttempts同 2.1.4 小節。
2.3.5 merge同 2.1.5 小節。
2.3.6 小結這種策略比較簡單,應該不用多做解釋吧。如果有多個 Realm 認證成功,這裡也是會返回多個 Realm 的認證信息的,獲取多個 Realm 的認證信息同上一小節。
2.4 FirstSuccessfulStrategy2.4.1 beforeAllAttempts
public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
return null;
}
不同于前面,這裡直接返回了 null。
2.4.2 beforeAttempt
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
if (getStopAfterFirstSuccess() && aggregate != null && !isEmpty(aggregate.getPrincipals())) {
throw new ShortCircuitIterationException();
}
return aggregate;
}
這裡的邏輯是這樣,如果 getStopAfterFirstSuccess() 方法返回 true,并且當前認證結果的聚合不為空,那麼就直接抛出異常,一旦抛出異常,就會跳出當前循環,也就是不會調用當前 Realm 進行認證操作了。這個思路和 FirstSuccessfulStrategy 名字基本上是契合的。
不過這裡有一個方法 getStopAfterFirstSuccess(),看名字就知道是否在第一次成功後停止認證,默認情況下,該變量為 false,即即使第一次認證成功後,也還是會繼續後面 Realm 的認證。
如果我們希望當第一次認證成功後,後面的 Realm 就不認證了,那麼記得配置該屬性為 true。
2.4.3 afterAttempt同 2.1.3 小節。
2.4.4 afterAllAttempts同 2.1.4 小節。
2.4.5 merge不知道小夥伴們是否還記得 merge 方法是在哪裡調用的,回顧 2.1.3 小節,如果當前 Realm 的認證和聚合結果都不為 null,就需要對結果進行合并,原本的合并是真正的去合并,這裡重寫了該方法,就沒有去執行合并了:
protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
if (aggregate != null && !isEmpty(aggregate.getPrincipals())) {
return aggregate;
}
return info != null ? info : aggregate;
}
這是三個策略中,唯一重寫 merge 方法的。
這裡的 merge 并沒有真正的 merge,而是:
可以看到,這裡的 merge 其實就是挑選一個認證的 info 返回。如果前面有認證成功的 Realm,後面 Realm 認證成功後返回的 info 是不會被使用的。
2.4.6 小結好啦,現在小夥伴們可以總結出 FirstSuccessfulStrategy 和 AtLeastOneSuccessfulStrategy 的區别了:
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
<property name="authenticator">
<bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy">
<property name="stopAfterFirstSuccess" value="true"/>
</bean>
</property>
<property name="realms">
<list>
<ref bean="myRealm01"/>
<ref bean="myRealm02"/>
</list>
</property>
</bean>
</property>
</bean>
好啦,這就是松哥和大家分享的 Shiro 多 Realm 情況,感興趣的小夥伴可以去試試哦~
公衆号後台回複 shiro,獲取 Shiro 相關資料。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!