Spring?Security認(rèn)證的完整流程記錄
前言
本文以用戶名/密碼驗證方式為例,講解 Spring Security 的認(rèn)證流程,在此之前,需要你了解 Spring Security 用戶名/密碼認(rèn)證的基本配置。
Spring Security 是基于過濾器的,通過一層一層的過濾器,處理認(rèn)證的流程,攔截非法請求。
認(rèn)證上下文的持久化
處于最前面的過濾器叫做 SecurityContextPersistenceFilter,Spring Security 是通過 Session 來存儲認(rèn)證信息的,這個過濾器的 doFilter 方法在每次請求中只執(zhí)行一次,作用就是,在請求時,將 Session 中的 SecurityContext 放到當(dāng)前請求的線程中(如果有),在響應(yīng)時,檢查縣城中是否有 SecurityContext,有的話將其放入 Session??梢岳斫鉃閷?SecurityContext 進(jìn)行 Session 范圍的持久化。
認(rèn)證信息的封裝
接著進(jìn)入 UsernamePasswordAuthenticationFilter,這是基于用戶名/密碼認(rèn)證過程中的主角之一。
默認(rèn)情況下,這個過濾器會匹配路徑為 /login 的 POST 請求,也就是 Spring Security 默認(rèn)的用戶名和密碼登錄的請求路徑。
這里最關(guān)鍵的代碼是 attemptAuthentication 方法(由 doFilter 方法調(diào)用),源碼如下:
@Override
public Authentication attemptAuthentication ( HttpServletRequest request, HttpServletResponse response )
throws AuthenticationException {
if ( this.postOnly && !request.getMethod () .equals ( "POST" )) {
throw new AuthenticationServiceException ( "Authentication method not supported: " + request.getMethod ()) ;
}
String username = obtainUsername ( request ) ;
username = ( username != null ) ? username : "";
username = username.trim () ;
String password = obtainPassword ( request ) ;
password = ( password != null ) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken ( username, password ) ;
// Allow subclasses to set the "details" property
setDetails ( request, authRequest ) ;
return this.getAuthenticationManager () .authenticate ( authRequest ) ;
}在 attemptAuthentication 方法代碼的第 12 行,使用從 request 中獲取到的用戶名和密碼,構(gòu)建了一個 UsernamePasswordAuthenticationToken 對象,我們可以看到這個構(gòu)造方法的代碼,非常簡單:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}只是保存了用戶名和密碼的引用,并且將認(rèn)證狀態(tài)設(shè)置為 false,因為此時只是封裝了認(rèn)證信息,還沒有進(jìn)行認(rèn)證。
我們再回到 attemptAuthentication 的代碼,在方法的最后一行,將創(chuàng)建好的認(rèn)證信息,傳遞給了一個 AuthenticationManager 進(jìn)行認(rèn)證。這里實際工作的是 AuthenticationManager 的實現(xiàn)類 ProviderManager。
查找處理認(rèn)證的 Provider 類
進(jìn)入 ProviderManager 可以從源碼中找到 authenticate 方法,代碼比較長,我就不貼在這里了,你可以自行查找,我簡述一下代碼中的邏輯。
ProviderManager 本身不執(zhí)行認(rèn)證操作,它管理著一個 AuthenticationProvider 列表,當(dāng)需要對一個封裝好的認(rèn)證信息進(jìn)行認(rèn)證操作的時候,它會將認(rèn)證信息和它管理者的 Provider 們,逐一進(jìn)行匹配,找到合適的 Provider 處理認(rèn)證的具體工作。
可以這樣理解,ProviderManager 是一個管理者,管理著各種各樣的 Provider。當(dāng)有工作要做的時候,它從來都不親自去做,而是把不同的工作,分配給不同的 Provider 去操作。
最后,它會將 Provider 的工作成果(已認(rèn)證成功的信息)返回,或者拋出異常。
那么,它是怎么將一個認(rèn)證信息交給合適的 Provider 的呢?
在上一部分中,我們說到,認(rèn)證信息被封裝成了一個 UsernamePasswordAuthenticationToken,它是Authentication 的子類,ProviderManager 會將這個認(rèn)證信息的類型,傳遞個每個 Provider 的 supports 方法,由 Provider 來告訴 ProviderManager 它是不是支持這個類型的認(rèn)證信息。
認(rèn)證邏輯
在 Spring Security 內(nèi)置的 Provider 中,與 UsernamePasswordAuthenticationToken 對應(yīng)的 Provider 是 DaoAuthenticationProvider,authenticate 方法在它的父類 AbstractUserDetailsAuthenticationProvider 中。我們來看它的 authenticate 方法:
@Override
public Authentication authenticate ( Authentication authentication ) throws AuthenticationException {
Assert.isInstanceOf( UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage ( "AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported" )) ;
String username = determineUsername ( authentication ) ;
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache ( username ) ;
if ( user == null ) {
cacheWasUsed = false;
try {
user = retrieveUser ( username, ( UsernamePasswordAuthenticationToken ) authentication ) ;
}
catch ( UsernameNotFoundException ex ) {
this.logger.debug ( "Failed to find user '" + username + "'" ) ;
if ( !this.hideUserNotFoundExceptions ) {
throw ex;
}
throw new BadCredentialsException ( this.messages
.getMessage ( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials" )) ;
}
Assert.notNull( user, "retrieveUser returned null - a violation of the interface contract" ) ;
}
try {
this.preAuthenticationChecks.check ( user ) ;
additionalAuthenticationChecks ( user, ( UsernamePasswordAuthenticationToken ) authentication ) ;
}
catch ( AuthenticationException ex ) {
if ( !cacheWasUsed ) {
throw ex;
}
// 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 ) ;
this.preAuthenticationChecks.check ( user ) ;
additionalAuthenticationChecks ( user, ( UsernamePasswordAuthenticationToken ) authentication ) ;
}
this.postAuthenticationChecks.check ( user ) ;
if ( !cacheWasUsed ) {
this.userCache.putUserInCache ( user ) ;
}
Object principalToReturn = user;
if ( this.forcePrincipalAsString ) {
principalToReturn = user.getUsername () ;
}
return createSuccessAuthentication ( principalToReturn, authentication, user ) ;
}代碼比較長,我們說要點:
- 代碼第 12 行,通過
retrieveUser方法,獲得UserDetails信息,這個方法的具體實現(xiàn),可以在DaoAuthenticationProvider中找到,主要是通過UserDetailsService的loadUserByUsername方法,查找系統(tǒng)中的用戶信息。 - 代碼第 25 行,通過
preAuthenticationChecks.check方法,進(jìn)行了認(rèn)證前的一些校驗。校驗的具體實現(xiàn)可以在DefaultPreAuthenticationChecks內(nèi)部類中找到,主要是判斷用戶是否鎖定、是否可用、是否過期。 - 代碼第 26 行,通過
additionalAuthenticationChecks方法,對用戶名和密碼進(jìn)行了校驗。具體實現(xiàn)可以在DaoAuthenticationProvider中找到。 - 代碼第 39 行,通過
postAuthenticationChecks.check方法,校驗了密碼是否過期。具體實現(xiàn)可以在DefaultPostAuthenticationChecks內(nèi)部類中找到。 - 最后,如果以上校驗和認(rèn)證都沒有問題,則通過
createSuccessAuthentication方法,創(chuàng)建成功的認(rèn)證信息,并返回。此時,就成功通過了認(rèn)證。
在最后的 createSuccessAuthentication 方法中,會創(chuàng)建一個新的 UsernamePasswordAuthenticationToken 認(rèn)證信息,這個新的認(rèn)證信息的認(rèn)證狀態(tài)為 true。表示這是一個已經(jīng)通過的認(rèn)證。
這個認(rèn)證信息會返回到 UsernamePasswordAuthenticationFilter 中,并作為 attemptAuthentication 方法的結(jié)果。
在 doFilter 方法中,會根據(jù)認(rèn)證成功或失敗的結(jié)果,調(diào)用相應(yīng)的 Handler 類進(jìn)行后續(xù)的處理,最后,認(rèn)證的信息也會被保存在 SecurityContext 中,供后續(xù)使用。
總結(jié)
到此這篇關(guān)于Spring Security認(rèn)證流程的文章就介紹到這了,更多相關(guān)Spring Security認(rèn)證流程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IntelliJ IDEA進(jìn)行中文漢化的詳細(xì)教程(附圖文講解)
今天為大家?guī)淼氖?nbsp;IntelliJ IDEA 中文漢化教程以及中文插件包下載教程,經(jīng)常收到小伙伴在后臺給我留言,問 IDEA 怎么進(jìn)行中文漢化,因為很多小伙伴是剛?cè)腴T Java,看到 IDEA 菜單全英文有些不太適應(yīng),需要的朋友可以參考下2024-12-12
Red?Hat?安裝JDK與IntelliJ?IDEA的詳細(xì)過程
YUM是基于Red Hat的Linux發(fā)行版的一個強(qiáng)大而用戶友好的包管理工具,這篇文章主要介紹了Red?Hat安裝JDK與IntelliJ IDEA,需要的朋友可以參考下2023-08-08
Java使用正則表達(dá)式進(jìn)行匹配且對匹配結(jié)果逐個替換
這篇文章主要介紹了Java使用正則表達(dá)式進(jìn)行匹配且對匹配結(jié)果逐個替換,文章圍繞主題展開詳細(xì)的內(nèi)容戒殺,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09
XML Web 服務(wù) Eclipse實現(xiàn)sun-jaxws.xml文件的方法
在sun-jaxws.xml文件,可以配置endpoint、handler-chain等內(nèi)容,在這個文件中配置的內(nèi)容會覆蓋在Java代碼中使用注解屬性配置的的內(nèi)容,本文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2023-11-11
Java實現(xiàn)批量導(dǎo)出導(dǎo)入數(shù)據(jù)及附件文件zip包
這篇文章主要為大家詳細(xì)介紹了Java實現(xiàn)批量導(dǎo)出導(dǎo)入數(shù)據(jù)及附件文件zip包的方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一2022-09-09
SpringBoot接口返回數(shù)據(jù)脫敏(Mybatis、Jackson)
有時候,我們接口返回的數(shù)據(jù)需要做一些處理,有一些敏感數(shù)據(jù),本文主要介紹了SpringBoot接口返回數(shù)據(jù)脫敏(Mybatis、Jackson),具有一定的參考價值,感興趣的可以了解一下2024-07-07

