本文講解針對對Shiro有一定了解的同學,如有錯誤歡迎指正,感謝
@Date: 2021-02-04
Shiro版本(目前最新是1.7.1)
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.6.0</version>
</dependency>
<!--與之對應的starter版本是-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.6.0</version>
</dependency>
1. 相關配置
1.1 ShiroFilterChainDefinition
在配置
ShiroConfig
的
ShiroFilterChainDefinition
時,設定登入(我定義的url是
"/login"
)請求不會被自定義的filter處理(anon,匿名通路,帶有此标示,會被
anon
對應的過濾器處理,但是該過濾器會直接放行)。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiclRnblN2XjlGcjAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHLwsmaOBTUU10dRpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL4QTOzETM0AjM0AjMwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
1.2 多Realm
由于整合了JWT是以定義了兩個Realm
DbRealm
:用于處理登入請求
JwtRealm
:用于處理需要JWT認證、鑒權後的請求。(JwtCredentialsMatcher是我自定義的JwtRealm的密碼比對器)
1.3 CredentialsMatcher
密碼比對器重寫了
HashedCredentialsMatcher
(Hash散列密碼比對器),并注入
DbRealm
。其中
AdminDao
用于CRUD管理者(使用者)資訊。
雜湊演算法:MD5
散列次數:1024次
參數設定原因:
由于保證賬戶的安全性,資料庫中密碼采用密文存儲,是以,本文定義的注冊流程(如下)對密碼進行了相同的處理![]()
Springboot+Shiro+JWT+前後端分離:登入流程源碼分析1. 相關配置2. 請求流程分析
1.4 ModularRealmAuthenticator
由于采用了多realm,在Shiro自帶的認證器
ModularRealmAuthenticator
的
doMultiRealmAuthentication( )
方法中,捕獲了異常,而後續的
afterAllAttempts
調用了
AtLeastOneSuccessfulStrategy
中的
afterAllAttempts(token, aggregate)
,抛出了新的異常
AuthenticationException
上述結果會導緻
UnknownAccountException
、
IncorrectCredentialsException
等異常被捕獲後隻抛出
AuthenticationException
,當然,這是多realm會出現的情況,一個realm都是正常的(真的坑爹)。
解決方法:
重寫,删掉捕獲異常的地方,也即删除
doMultiRealmAuthentication()
就能重新捕獲
try...catch...
等異常。
UnknownAccountException
本節參考部落格:https://blog.csdn.net/dan339811953/article/details/104798079
2. 請求流程分析
2.1 登入Controller
2.1.1 Controller
放在service也可以,别杠了 = =
如注釋所述,顯然,認證流程是發生
subject.login(token)
中的。
再向裡追溯,進入
Subject
接口的實作類
DelegatingSubject
的
login(AuthenticationToken token)
方法
2.1.2 DelegatingSubject
這裡調用了安全管理器的
login(...)
方法
:subject主體,這是個抽象的概念,代表請求登入的Object(我了解為是一個需要進行登入操作的對象,有點像身份證,記錄了使用者名,密碼以及相關狀态)
this
:由使用者名和明文密碼生成
token
繼續深入
2.1.3 DefaultSecurityManager
調用了實作
SecurityManager
接口的
DefaultSecurityManager
類的
login(...)
方法
Info = authenticate(token) 是認證的後續實作方法
繼續深入
會走到
AuthenticatingDefaultSecurityManager
的
authenticate(AuthenticationToken token)
方法,這個方法在
AbstractAuthenticator
抽象類有實作
進入
doAuthenticat(token)
方法,會通過
AbstractAuthenticator
抽象類的繼承類
ModularRealmAuthenticator
2.1.4 ModularRealmAuthenticator
這裡會根據realm的數量,調用不同的方法。在1.2 和1.4中,顯然是會進入
doMultiRealmAuthentication(realms, authenticationToken)
realms
:一個集合,存放自定義的realm
authenticationToken
:之前生成的UserPasswordToken
這裡會走1.4中自定義的
CustomModularRealmAuthenticator
(名字自定義的)
直接上代碼,解說放在注釋裡了:
/**
* 為什麼定義此類?
* 自定義重寫ModularRealmAuthenticator類,用與處理多realm的自定義異常捕獲問題,因為shiro自帶的多realm會将異常捕獲
* 主要是在{@link ModularRealmAuthenticator}中的{@code doAuthenticate}方法中判斷了是否是多realm,走了不同的認證流程
* 在多realm認證中{@code doMultiRealmAuthentication}捕獲了異常,而後續的{@code afterAllAttempts}
* 調用了{@link AtLeastOneSuccessfulStrategy}中的{@code afterAllAttempts},抛出了新的異常{@code AuthenticationException}
* 解決辦法:
* 隻需去除中間捕獲異常的過程
* @date 2021/2/3 下午1:53
*/
@Slf4j
public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
// 政策:ShiroConfig中配置了,我采用的是FirstSuccessfulStrategy
//
// shiro自帶的三種政策:
// 1. FirstSuccessfulStrategy:隻要有一個Realm驗證成功即可,隻傳回第一個成功驗證身份的Realm認證資訊,其他的忽略;
// 2. AtLeastOneSuccessfulStrategy:(預設)隻要有一個Realm驗證成功即可,和FirstSuccessfulStrategy 不同,
// 傳回所有 Realm 身份驗證成功的認證資訊;
// 3. AllSuccessfulStrategy:所有Realm驗證成功才算成功,且傳回所有Realm身份驗證成功的 認證資訊,如果有一個失敗就失敗了。
AuthenticationStrategy strategy = getAuthenticationStrategy();
// FirstSuccessfulStrategy中傳回的是null
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
// 周遊每個realm
for (Realm realm : realms) {
// 通過realm中重寫的support方法可以差別不同類型的token,因為是內建了jwt,是以還有jwt
aggregate = strategy.beforeAttempt(realm, token, aggregate);
// 判斷realm是否支援token,在自定義的兩個realm中,肯定隻有DbRealm支援
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
Throwable t = null;
/*
* 此處執行取消try catch,若異常直接抛出,其他同原方法
*/
// 這裡會去AuthenticatingRealm中判讀擷取AuthenticationInfo
// 首先是getCachedAuthenticationInfo,但是沒有定義緩存的AuthenticationInfo,是以跳過,
// 接下來,也就是自定義Realm時重寫的doGetAuthenticationInfo(AuthenticationToken token)方法
AuthenticationInfo info = realm.getAuthenticationInfo(token);
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
把上述第43行的代碼拿出來分析
2.1.5 AuthenticatingRealm
擷取緩存的
AuthenticationInfo
,因為還沒內建Redis,是以還沒做緩存。Shiro雖然自帶有CacheManager,但預設是不緩存AuthenticationInfo的。
進入自定義的
DbRealm的doGetAuthenticationInfo(AuthenticationToken token)
2.1.5.1 DbRealm
token
:請求時,生成的帶有使用者名和明文密碼的
UsernamePasswordToken
AuthenticationInfo
: 資料庫中查出的使用者名、密文密碼、鹽(在1.3中有說明)
繼續回到
2.1.5 AuthenticatingRealm
中,流程向下,走到了
assertCredentialsMatch(token, info)
,至此,明白終于在這個方法裡,通過密碼比對器來驗證帶明文密碼的token經過散列後得到的密文,是否和info中資料庫儲存的密文密碼相同。
進入這個方法
首先是擷取密碼比對器,在1.3中已說明我配置的的DbRealm的密碼比對器
HashedCredentialsMatcher
,是以執行
cm.doCredentialsMatch(token, info)
2.1.6 HashedCredentialsMatcher
這裡是把‘明文加鹽散列化後的密文’ 和‘資料庫中儲存密碼‘由String類型轉為Char數組,以保證安全性。可參考下面這個部落格
參考部落格:https://blog.csdn.net/u012881904/article/details/53843386
為什麼Java中的密碼優先使用 char[] 而不是String?
知乎:https://www.zhihu.com/question/36734157
防止對方文檔丢失,做了搬運 = =
String在Java中是不可變對象,如果作為普通文本存儲密碼,那麼它會一直存在記憶體中直至被垃圾收集器回收。這就意味着一旦建立了一個字元串,如果另一個程序把嘗試記憶體的資料導出(dump),在GC進行垃圾回收之前該字元串會一直保留在記憶體中,那麼該程序就可以輕易的讀取到該字元串。
而對于數組,可以在使用該數組之後顯示地擦掉數組中的内容,你可以使用其他不相關的内容把數組内容覆寫掉,例如,在使用完密碼後,我們将char[]的值均賦為0,如果有人能以某種方式看到記憶體映像,他隻能看到一串0;而如果我們使用的是字元串,他們便能以純文字方式看到密碼。是以,使用char[]是相對安全的。
推薦使用char[],這是從安全角度來選擇的。但是,我們應當注意到,即使是用char[]處理密碼也隻是降低被攻擊的機率而已,還是會有其他方法攻破數組處理的密碼。
另一方面,使用String的時候,你可能會不經意間将密碼列印出來(如log檔案),此時,使用char[]就顯得更加的安全了,如:
public static void main(String[] args) { Object pw = “Password”; System.out.println(“String: ” + pw); }
此時的輸出結果将會是pw = "Password".toCharArray(); System.out.println("Array: " + pw);
實際上,即使使用了char[]儲存密碼也仍然不夠安全,記憶體中還是可能會有這串資料的零碎副本,是以,建議使用加密的密碼來代替普通的文本字元串密碼,并且在使用完後記得立即清除。String: Password Array: [C@5829428e
3. 尾聲
在
equals(...)
中發現了一個有趣的算法,可以拿來做字元串比較麼?畢竟是官方源碼
翻一下說明:
在位元組數組a中的所有位元組都要被确認相等。算法計算的時間僅僅依賴于a數組的長度,與b的長度或a與b的内容無關。
package java.security
// MessageDigest.java中
public static boolean isEqual(byte[] digesta, byte[] digestb) {
/* All bytes in digesta are examined to determine equality.
* The calculation time depends only on the length of digesta
* It does not depend on the length of digestb or the contents
* of digesta and digestb.
*/
if (digesta == digestb) return true;
if (digesta == null || digestb == null) {
return false;
}
int lenA = digesta.length;
int lenB = digestb.length;
if (lenB == 0) {
return lenA == 0;
}
int result = 0;
result |= lenA - lenB;
// time-constant comparison
for (int i = 0; i < lenA; i++) {
// If i >= lenB, indexB is 0; otherwise, i.
int indexB = ((i - lenB) >>> 31) * i;
result |= digesta[i] ^ digestb[indexB];
}
return result == 0;
}