Spring Security 解析(二) —— 認(rèn)證過程
??在學(xué)習(xí)Spring Cloud 時(shí),遇到了授權(quán)服務(wù)oauth 相關(guān)內(nèi)容時(shí)伦忠,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth2 等權(quán)限、認(rèn)證相關(guān)的內(nèi)容若河、原理及設(shè)計(jì)學(xué)習(xí)并整理一遍。本系列文章就是在學(xué)習(xí)的過程中加強(qiáng)印象和理解所撰寫的寞宫,如有侵權(quán)請告知萧福。
項(xiàng)目環(huán)境:
- JDK1.8
- Spring boot 2.x
- Spring Security 5.x
一、@EnableGlobalAuthentication 配置 解析
??還記得上一篇講解授權(quán)過程中提到@EnableWebSecurity 引用了 WebSecurityConfiguration 配置類 和 @EnableGlobalAuthentication 注解嗎辈赋? 當(dāng)時(shí)只是講解了下 WebSecurityConfiguration 配置類 鲫忍,這次該輪到 @EnableGlobalAuthentication 配置了。
??查看 @EnableGlobalAuthentication 注解源碼钥屈,我們可以看到其引用了AuthenticationConfiguration 配置類悟民。其中有一個(gè)方法值得我們注意,那就是 getAuthenticationManager() (還記得授權(quán)過程中調(diào)用了 AuthenticationManager().authenticate() 進(jìn)行認(rèn)證么焕蹄?), 我們來看下其源碼內(nèi)部大致邏輯:
public AuthenticationManager getAuthenticationManager() throws Exception {
......
// 1 調(diào)用 authenticationManagerBuilder 方法獲取 authenticationManagerBuilder 對象逾雄,用于 build authenticationManager 對象
AuthenticationManagerBuilder authBuilder = authenticationManagerBuilder(
this.objectPostProcessor, this.applicationContext);
.....
// 2 build 方法調(diào)用同授權(quán)過程中的 webSecurity.build() 一樣阀溶,都是通過父類 AbstractConfiguredSecurityBuilder.doBuild() 方法中的 performBuild() 方法進(jìn)行 build, 只是這里不再是通過其子類 HttpSecurity.performBuild() 腻脏,而是通過 AuthenticationManagerBuilder.performBuild()
authenticationManager = authBuilder.build();
.......
return authenticationManager;
}
根據(jù)源碼我們可以概括其邏輯分2部分:
- 1、 通過調(diào)用 authenticationManagerBuilder() 方法獲取 authenticationManagerBuilder 對象
- 2银锻、 調(diào)用authenticationManagerBuilder 對象的 build() 創(chuàng)建 authenticationManager 對象并返回
??我們再詳細(xì)看下這個(gè)build的過程永品,可以發(fā)現(xiàn)其 build 調(diào)用跟授權(quán)過程中build securityFilterChain 一樣 都是通過 AbstractConfiguredSecurityBuilder.doBuild() 方法中的 performBuild() 進(jìn)行構(gòu)建, 不過這次不再是調(diào)用其子類 HttpSecurity.performBuild() 而是 AuthenticationManagerBuilder.performBuild() 击纬。
我們來看下 AuthenticationManagerBuilder.performBuild() 方法內(nèi)部實(shí)現(xiàn):
protected ProviderManager performBuild() throws Exception {
if (!isConfigured()) {
logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
return null;
}
// 1 創(chuàng)建了一個(gè)包含 authenticationProviders 參數(shù) 的 ProviderManager 對象
ProviderManager providerManager = new ProviderManager(authenticationProviders,
parentAuthenticationManager);
if (eraseCredentials != null) {
providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
}
if (eventPublisher != null) {
providerManager.setAuthenticationEventPublisher(eventPublisher);
}
providerManager = postProcess(providerManager);
return providerManager;
}
?? 這里我們主要關(guān)注其內(nèi)部 創(chuàng)建了一個(gè)包含 authenticationProviders 參數(shù) 的 ProviderManager (ProviderManager 是 AuthenticationManager 的實(shí)現(xiàn)類)對象并返回鼎姐。
回過頭,我們來看下 AuthenticationManager 接口 源碼:
public interface AuthenticationManager {
// 認(rèn)證接口
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
?? 可以看到更振,內(nèi)部就只有一個(gè)我們在授權(quán)過程中提到過的 authenticate()炕桨,其接口接收一個(gè) Authentication(這個(gè)對象我們也不陌生,之前授權(quán)過程中提到過的 UsernamePasswordAuthrnticationToken 等都是其實(shí)現(xiàn)子類) 對象作為參數(shù)肯腕。
??至此認(rèn)證的部分關(guān)鍵類或接口已經(jīng)浮出水面了献宫,它們分別是 AuthenticationManager 、ProviderManager实撒、AuthenticationProvider姊途、Authentication, 接下來我們就圍繞這幾個(gè)類或接口進(jìn)行剖析知态。
二捷兰、AuthenticationManager
??正如我們之前看到的一項(xiàng),它是整個(gè)認(rèn)證的入口负敏,其定義的認(rèn)證接口 authenticate() 接收一個(gè) Authentication 對象作為參數(shù)贡茅。AuthenticationManager 它只是提供了一個(gè)認(rèn)證接口方法,因?yàn)樵趯?shí)際使用中,我們不僅有賬戶密碼的登錄方式顶考,還有短信驗(yàn)證碼登錄彤叉、郵箱登錄等等,所以它本身不做任何認(rèn)證村怪,其具體做認(rèn)證的是 ProviderManager 子類秽浇,但正如我們說過的認(rèn)證方式有很多,如果僅僅依靠 ProviderManager 本身來實(shí)現(xiàn) authenticate() 接口甚负,那我們要支持這么多認(rèn)證方式不得寫多少個(gè) if 判斷柬焕,而且以后如果我們想要支持指紋登錄,那又不得不在這個(gè)方法內(nèi)部加個(gè)if梭域,這種不利于系統(tǒng)擴(kuò)展的寫法肯定是不可取的斑举,所以 ProviderManager 本身會維護(hù)一個(gè)List<AuthenticationProvider>列表 ,用于存放多種認(rèn)證方式病涨,然后通過委托的方式富玷,調(diào)用 AuthenticationProvider 來真正實(shí)現(xiàn)認(rèn)證邏輯的 。 而 Authentication 就是我們需要認(rèn)證的信息(當(dāng)然不僅僅只包括賬戶信息)既穆,通過authenticate() 接口認(rèn)證成功后返回的 Authentication 就是一個(gè)被標(biāo)識認(rèn)證成功的對象 赎懦。 這里為什么要解釋下 AuthenticationManager、ProviderManager幻工、AuthenticationProvider 的關(guān)系励两,主要是一開始容易搞混它們,相信經(jīng)過這樣一段描述更容易理解了吧囊颅。当悔。。
三踢代、Authentication
?? 如果 沒有看過源碼的同學(xué)可能會認(rèn)為 Authentication 是一個(gè)類吧盲憎,可實(shí)際上它是一個(gè) 接口,其內(nèi)部并未存在任何屬性字段胳挎,它僅僅定義了和規(guī)范好了認(rèn)證對象需要的接口方法饼疙,我們來看看其定義的接口方法有哪些,分別又什么作用:
public interface Authentication extends Principal, Serializable {
// 1 獲取權(quán)限信息(不能僅僅理解未角色權(quán)限串远,還有菜單權(quán)限等等)宏多,默認(rèn)是GrantedAuthority接口的實(shí)現(xiàn)類
Collection<? extends GrantedAuthority> getAuthorities();
// 2 獲取用戶密碼信息 ,認(rèn)證成功后會被刪除掉
Object getCredentials();
// 3 主要存放訪問著的ip等信息
Object getDetails();
// 4 重點(diǎn)T璺!伸但! 最重要的身份信息。 大部分情況下是 UserDetails 接口的實(shí)現(xiàn) 類留搔,比如 我們 之前配置的 User 對象
Object getPrincipal();
// 5 是否認(rèn)證(成功)
boolean isAuthenticated();
// 6 設(shè)置認(rèn)證標(biāo)識
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
?? 既然 Authentication 定義了這些接口方法更胖,那么其子類實(shí)現(xiàn)肯定都按照這個(gè)標(biāo)準(zhǔn)或者稱之為規(guī)范定制了實(shí)現(xiàn),這里就不羅列出其子類的具體實(shí)現(xiàn)了,有興趣的同學(xué)可以去看下 我們最常用的 UsernamePasswordAuthenticationToken 實(shí)現(xiàn)(包括其 父類 AbstractAuthenticationToken)
四却妨、ProviderManager
?? 它是 AuthenticationManager 的實(shí)現(xiàn)子類之一饵逐,也是我們最常用的一個(gè)實(shí)現(xiàn)。正如我們前面提到過的彪标,其內(nèi)部維護(hù)了 一個(gè) List<AuthenticationProvider> 對象倍权, 用于支持和擴(kuò)展 多種形式的認(rèn)證方式。我們來看下 其 實(shí)現(xiàn) authenticate() 的源碼:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
......
// 1 通過 getProviders() 方法獲取到內(nèi)部維護(hù)的 List<AuthenticationProvider> 對象 并 通過遍歷的方式 去 認(rèn)證捞烟,只要認(rèn)證成功 就 break
for (AuthenticationProvider provider : getProviders()) {
// 2 正如前面看到的有 很多 AuthenticationProvider 實(shí)現(xiàn)薄声,如果每次都是驗(yàn)證失敗后再掉用下一個(gè) AuthenticationProvider 這種實(shí)現(xiàn)是不是很不高效? 所以 這里通過 supports() 方法來驗(yàn)證是否可以使用 該 AuthenticationProvider 進(jìn)行驗(yàn)證题画,不可以就直接換下一個(gè)
if (!provider.supports(toTest)) {
continue;
}
try {
// 3 重點(diǎn)默辨,這里是 調(diào)用真實(shí)的認(rèn)證方法
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
try {
// 4 前面都認(rèn)證不成功,調(diào)用父類(嚴(yán)格意思不是調(diào)用父類苍息,而是其他的 AuthenticationManager 實(shí)現(xiàn)類)認(rèn)證方法
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 5 刪除認(rèn)證成功后的 密碼信息缩幸,保證安全
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
?? 梳理下整個(gè)方法內(nèi)部實(shí)現(xiàn)邏輯:
- 通過 getProviders() 方法獲取到內(nèi)部維護(hù)的 List<AuthenticationProvider> 對象 并 通過遍歷的方式 去 認(rèn)證
- 通過 provider.supports() 方法 來驗(yàn)證是否可用當(dāng)前的 AuthenticationProvider 進(jìn)行驗(yàn)證,不可以就直接換下一個(gè) ( 其實(shí)方法內(nèi)部就是驗(yàn)證當(dāng)前 的 Authentication 對象是不是其某個(gè)子類竞思,比如 我們最常用到的 DaoAuthenticationProvider 的 supports 方法就是判斷當(dāng)前 的 Authentication 是不是 UsernamePasswordAuthenticationToken )
- 通過 provider.authenticate() 調(diào)用 其真正的認(rèn)證實(shí)現(xiàn)
- 如果 前面的所有 AuthenticationProvider 均不能認(rèn)證成功表谊,嘗試調(diào)用 parent.authenticate() 方法 :調(diào)用父類(嚴(yán)格意思不是調(diào)用父類,而是其他的 AuthenticationManager 實(shí)現(xiàn)類)認(rèn)證方法
- 最后 通過 ((CredentialsContainer) result).eraseCredentials() 刪除認(rèn)證成功后的 密碼信息衙四,保證安全
五铃肯、AuthenticationProvider(DaoAuthenticationProvider)
?? 正如我們想象的一樣,AuthenticationProvider 是一個(gè)接口传蹈,本身定義了一個(gè) 和 AuthenticationManager 一樣的 authenticate 認(rèn)證接口方法,外加一個(gè) supports() 用于 判別當(dāng)前 Authentication 是否可以進(jìn)行處理步藕。
public interface AuthenticationProvider {
// 定義認(rèn)證接口方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
// 定義判斷是否可以認(rèn)證處理的接口方法
boolean supports(Class<?> authentication);
}
?? 這里我們就拿我們用得最多的一個(gè) AuthenticationProvider 實(shí)現(xiàn)類 DaoAuthenticationProvider(注意惦界,這里和UsernamePasswordAuthenticationFilter 類似,都是通過父類來實(shí)現(xiàn)接口咙冗,然后內(nèi)部處理方法再調(diào)用 其 子類進(jìn)行處理) 來看其內(nèi)部 這2個(gè)抽象方法的實(shí)現(xiàn):
- supports 實(shí)現(xiàn):
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
?? 可以看到僅僅只是判斷當(dāng)前的 authentication 是否為 UsernamePasswordAuthenticationToken(或其子類)
- authrnticate 實(shí)現(xiàn)
// 1 注意這里的實(shí)現(xiàn)方法是 DaoAuthenticationProvider 的父類 AbstractUserDetailsAuthenticationProvider 實(shí)現(xiàn)的
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 2 從 authentication 中獲取 用戶名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 3 根據(jù)username 從緩存中獲取 認(rèn)證成功的 UserDetails 信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 4 如果緩存中沒有用戶信息 需要 獲取用戶信息(由 DaoAuthenticationProvider 實(shí)現(xiàn) )
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
......
}
}
try {
// 5 前置檢查賬戶是否鎖定沾歪,過期,凍結(jié)(由DefaultPreAuthenticationChecks類實(shí)現(xiàn))
preAuthenticationChecks.check(user);
// 6 主要是驗(yàn)證 獲取到的用戶密碼與傳入的用戶密碼是否一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
// 這里官方發(fā)現(xiàn)緩存可能導(dǎo)致了某些問題雾消,又重新去認(rèn)證一次
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 7 后置檢查用戶密碼是否 過期
postAuthenticationChecks.check(user);
// 8 驗(yàn)證成功后的用戶信息存入緩存
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 9 重新創(chuàng)建一個(gè) authenticated 為true (即認(rèn)證成功)的 UsernamePasswordAuthenticationToken 對象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
?? 梳理下authenticate(這里的方法的實(shí)現(xiàn)是由 AbstractUserDetailsAuthenticationProvider 提供的)方法內(nèi)部實(shí)現(xiàn)邏輯:
- 從 入?yún)?authentication 對象中獲取到 username 信息
- (這里忽略緩存的處理) 調(diào)用 retrieveUser() 方法(由 DaoAuthenticationProvider 實(shí)現(xiàn))根據(jù) username 獲取到 系統(tǒng)(一般來說是從數(shù)據(jù)庫中) 中獲取到 UserDetails 對象
- 通過 preAuthenticationChecks.check() 方法檢測 當(dāng)前獲取到的 UserDetails 是否過期灾搏、凍結(jié)、鎖定(如果任意一個(gè)條件 為 true 將拋出 相應(yīng) 的異常)
- 通過 additionalAuthenticationChecks() (由 DaoAuthenticationProvider 實(shí)現(xiàn)) 判斷 密碼是否一致
- 通過 postAuthenticationChecks.check() 檢測 UserDetails 的密碼是否過期
- 最后通過 createSuccessAuthentication() 重新創(chuàng)建一個(gè) authenticated 為true (即認(rèn)證成功)的 UsernamePasswordAuthenticationToken 對象并返回
??雖然我們知道其驗(yàn)證邏輯立润, 但其內(nèi)部很多方法我們不清楚其內(nèi)部實(shí)現(xiàn),以及這里新增的一個(gè) 關(guān)鍵認(rèn)證類 UserDetails 是怎么設(shè)計(jì)的,如何驗(yàn)證其是否過期等等咳焚。
六津函、 UserDetailsService 和 UserDetails
??繼續(xù)深入看下 retrieveUser() 方法,首先我們注意到其返回對象是一個(gè) UserDetails,那么我們先從 UserDetails 入手。
UserDetails:
?? 我們先來看下 UserDetails 源碼:
public interface UserDetails extends Serializable {
// 1 與 Authentication 的 一樣丛晦,都是獲取 權(quán)限信息
Collection<? extends GrantedAuthority> getAuthorities();
// 2 獲取用戶正確的密碼
String getPassword();
// 3 獲取賬戶名
String getUsername();
// 4 賬戶是否過期
boolean isAccountNonExpired();
// 5 賬戶是否鎖定
boolean isAccountNonLocked();
// 6 密碼是否過期
boolean isCredentialsNonExpired();
// 7 賬戶是否凍結(jié)
boolean isEnabled();
}
?? 從上面的 4奕纫,5,6烫沙,7 接口我們就能夠知道 preAuthenticationChecks.check() 和 postAuthenticationChecks.check() 是如何檢測的了匹层,這里2個(gè)方法的檢測細(xì)節(jié)就不再深究了,有興趣的同學(xué)可以看看源碼锌蓄,我們只要知道檢測失敗會拋出異常就行了又固。
??咋呼一看,這個(gè)UserDetails 和 Authentication 很相似煤率,其實(shí)它們之間還真有關(guān)系仰冠,在createSuccessAuthentication() 傳教Authentication 對象時(shí),它的authorities 就是UserDetails 傳入的蝶糯。
UserDetailsService:
??retrieveUser() 方法是系統(tǒng)通過傳入的賬戶名獲取對應(yīng)的賬戶信息的唯一方法洋只,我們來看下其內(nèi)部源碼邏輯:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 通過 UserDetailsService 的loadUserByUsername 方法 獲取用戶信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
......
}
}
?? 相信看到這里,一切都關(guān)聯(lián)上了昼捍,這里的 UserDetailsService.loadUserByUsername() 就是我們在 上一篇 授權(quán)過程中 我們自己實(shí)現(xiàn)的识虚。 這里就不再 貼出UserDetailsService 源碼了。
?? 還有additionalAuthenticationChecks() 密碼驗(yàn)證沒有講到妒茬,這里簡單提下担锤,其內(nèi)部就是通過 PasswordEncoder.matches() 方法進(jìn)行密碼匹配的。不過這里要注意一下乍钻,這里的 PasswordEncoder 在 Security 5 開始默認(rèn) 替換成了 DelegatingPasswordEncoder 這里也是和我們之前 討論 loadUserByUsername 方法內(nèi)部創(chuàng)建User (UserDeatails 實(shí)現(xiàn)類之一)是一定要用到 PasswordEncoderFactories.createDelegatingPasswordEncoder().encode() 加密是相應(yīng)的肛循。
七、個(gè)人總結(jié)
?? 認(rèn)證的頂級管理員 AuthenticationManager 為我們提供了 認(rèn)證入口( authenticate()接口)银择,但是呢多糠,我們也知道大老板一般不直接參與實(shí)質(zhì)的工作,所以它把任務(wù)安排給它的下屬浩考,也就是我們的 ProviderManager 部門領(lǐng)導(dǎo) 夹孔,部門領(lǐng)導(dǎo) 肩負(fù)起 認(rèn)證的工作(authenticate() 認(rèn)證的實(shí)現(xiàn)),其實(shí)呢析孽,我們也知道部門領(lǐng)導(dǎo)也是 直接參數(shù) 認(rèn)證工作的搭伤,它都是將實(shí)際任務(wù)安排給小組長的, 也就是我們的 AuthrnticationProvider 袜瞬,部門領(lǐng)導(dǎo) 開個(gè)會議怜俐,聚集了所有小組長 ,讓它們自行判斷(通過
support()) 大老板交下來的任務(wù) 該由誰來完成吞滞, 小組長 領(lǐng)到任務(wù)后佑菩,就把任務(wù) 分發(fā)給各個(gè)小組成員盾沫,比如 成員1(UserDetailsService) 只需要 完成 retrieveUser() 的工作,然后成員2 完成 additionalAuthenticationChecks() 的工作殿漠,最后由項(xiàng)目經(jīng)理 ( createSuccessAuthentication() ) 將結(jié)果匯報(bào)給小組長赴精,然后小組長匯報(bào)給部門領(lǐng)導(dǎo),部門領(lǐng)導(dǎo) 審核一下結(jié)果绞幌,覺得小組長做得不夠好蕾哟,然后又做了一些操作 ( eraseCredentials() 擦除密碼信息 ),最后認(rèn)為 結(jié)果 可以了就匯報(bào)給老板莲蜘,老板呢谭确,也不多看,直接將結(jié)果給了客戶(filter)票渠。
?? 按照慣例逐哈,上流程圖:
?? 本文介紹認(rèn)證過程的代碼可以訪問代碼倉庫中的 security 模塊 ,項(xiàng)目的github 地址 : https://github.com/BUG9/spring-security
?? ?? ?? 如果您對這些感興趣问顷,歡迎star昂秃、follow、收藏杜窄、轉(zhuǎn)發(fā)給予支持肠骆!