tft每日頭條

 > 科技

 > 手機qq上面怎麼登錄qq郵箱

手機qq上面怎麼登錄qq郵箱

科技 更新时间:2024-11-28 13:45:09

今天想和大家聊一聊 Shiro 中的多 Realm 認證策略問題~

在項目中,如果我們想手機驗證碼登錄、第三方 QQ 登錄、郵箱登錄等多種登錄方式共存,那麼就可以考慮通過 Shiro 中的多 Realm 來實現,具體操作中,一個 Realm 剛好就對應一種登錄方式。

多 Realm 登錄的用法并不難,松哥之前也專門發過相關的文章和大家分享,傳送門:

  • 其實我不僅會 Spring Security,Shiro 也略懂一二!

今天我不想聊用法,主要是想和大家聊一聊這裡相關的源碼。因此本文需要大家有一定的 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); } }

這個方法的邏輯很簡單:

  1. 首先調用 assertRealmsConfigured 方法判斷一下開發者有沒有配置 Realm,要是沒有配置就直接抛異常了。
  2. 判斷開發者配置了幾個 Realm,要是配置了一個,就調用 doSingleRealmAuthentication 方法進行處理,要是配置了多個 Realm 則調用 doMultiRealmAuthentication 方法進行處理。

配置一個 Realm 的情況比較簡單,不在本文的讨論範圍内,本文主要是想和大家讨論多個 Realm 的情況。

當存在多個 Realm 的時候,必然又會帶來另一個問題:認證策略,即怎麼樣就算認證成功?一個 Realm 認證成功就算成功還是所有 Realm 認證成功才算成功?還是怎麼樣。

接下來我們來詳細聊一聊這個話題。

2. AuthenticationStrategy

先來整體上看下,負責認證策略的類是 AuthenticationStrategy,這是一個接口,有三個實現類:

手機qq上面怎麼登錄qq郵箱(手機短信登錄郵箱登錄)1

單從字面上來看,三個實現類都好理解:

  • AtLeastOneSuccessfulStrategy:至少有一個 Realm 認證成功。
  • AllSuccessfulStrategy:所有 Realm 都要認證成功。
  • FirstSuccessfulStrategy:這個從字面上理解不太準确,它是隻返回第一個認證成功的用戶數據

第二種其實很好理解,問題在于第 1 個和第 3 個,這兩個單獨理解也好理解,放在一起的話,那有人不禁要問,這倆有啥區别?

老實說,在 1.3.2 之前的版本還真沒啥大的區别,不過現在最新版本還是有些區别,且聽松哥來分析。

首先這裡一共涉及到四個方法:

  • beforeAllAttempts:在所有 Realm 驗證之前的做準備。
  • beforeAttempt:在單個 Realm 之前驗證做準備。
  • afterAttempt:處理單個 Realm 驗證之後的後續事宜。
  • afterAllAttempts:處理所有 Realm 驗證之後的後續事宜。

第一個和第四個方法在每次認證流程中隻調用一次,而中間兩個方法則在每個 Realm 調用前後都會被調用到,僞代碼就類似下面這樣:

手機qq上面怎麼登錄qq郵箱(手機短信登錄郵箱登錄)2

上面這四個方法,在 AuthenticationStrategy 的四個實現類中有不同的實現,我整理了下面一張表格,方便大家理解:

手機qq上面怎麼登錄qq郵箱(手機短信登錄郵箱登錄)3

大家注意這裡多了一個 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 認證結果的聚合,具體邏輯如下:

  1. 如果當前 Realm 認證結果為 null,則把聚合結果賦值給 info 并返回。
  2. 如果當前 Realm 認證結果不為 null,并且聚合結果為 null,那麼就把當前 Realm 的認證結果賦值給 info 并返回。
  3. 如果當前 Realm 認證結果不為 null,并且聚合結果也不為 null,則将兩者合并之後返回。
2.1.4 afterAllAttempts

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 的功能。

  1. 首先,系統調用 beforeAllAttempts 方法會獲取一個空的 SimpleAuthenticationInfo 對象作為聚合結果 aggregate。
  2. 接下來遍曆所有的 Realm,在每個 Realm 調用之前先調用 beforeAttempt 方法,該方法隻會原封不動的返回聚合結果 aggregate。
  3. 調用每個 Realm 的 getAuthenticationInfo 方法進行認證。
  4. 調用 afterAttempt 方法對認證結果進行聚合處理。如果當前 Realm 認證返回 null,就把聚合結果返回;如果當前 Realm 認證不返回 null,就把 當前的 Realm 的認證結果和 aggregate 進行合并(aggregate 不會為 null,因為 beforeAllAttempts 方法中固定返回空對象)。

這就是 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,而是:

  1. 如果聚合結果不為空,就直接返回聚合結果。
  2. 否則,如果當前認證結果不為空,就返回當前認證結果。
  3. 否則返回空。

可以看到,這裡的 merge 其實就是挑選一個認證的 info 返回。如果前面有認證成功的 Realm,後面 Realm 認證成功後返回的 info 是不會被使用的。

2.4.6 小結

好啦,現在小夥伴們可以總結出 FirstSuccessfulStrategy 和 AtLeastOneSuccessfulStrategy 的區别了:

  1. AtLeastOneSuccessfulStrategy:當存在多個 Realm 的時候,即使已經有一個 Realm 認證成功了,後面的 Realm 也還是會去認證,并且如果後面的 Realm 也認證成功了,那麼會将多個 Realm 認證成功的結果進行合并。
  2. FirstSuccessfulStrategy:當存在多個 Realm 的時候,默認情況下,即使已經有一個 Realm 認證成功了,後面的 Realm 也還是會去認證,但是如果後面的 Realm 也認證成功了,卻并不會使用後面認證成功的 Realm 返回的結果。如果我們希望當一個 Realm 認證成功後,後面的 Realm 就不再認證了,那麼可以配置 stopAfterFirstSuccess 屬性的值,配置方式如下:

<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>

3. 小結

好啦,這就是松哥和大家分享的 Shiro 多 Realm 情況,感興趣的小夥伴可以去試試哦~

公衆号後台回複 shiro,獲取 Shiro 相關資料。

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关科技资讯推荐

热门科技资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved