Spring Security認證的完整流程記錄
前言
本文以用戶名/密碼驗證方式為例,講解 Spring Security 的認證流程,在此之前,需要你瞭解 Spring Security 用戶名/密碼認證的基本配置。
Spring Security 是基於過濾器的,通過一層一層的過濾器,處理認證的流程,攔截非法請求。
認證上下文的持久化
處於最前面的過濾器叫做 SecurityContextPersistenceFilter
,Spring Security 是通過 Session 來存儲認證信息的,這個過濾器的 doFilter
方法在每次請求中隻執行一次,作用就是,在請求時,將 Session 中的 SecurityContext 放到當前請求的線程中(如果有),在響應時,檢查縣城中是否有 SecurityContext,有的話將其放入 Session。可以理解為將 SecurityContext 進行 Session 范圍的持久化。
認證信息的封裝
接著進入 UsernamePasswordAuthenticationFilter
,這是基於用戶名/密碼認證過程中的主角之一。
默認情況下,這個過濾器會匹配路徑為 /login
的 POST 請求,也就是 Spring Security 默認的用戶名和密碼登錄的請求路徑。
這裡最關鍵的代碼是 attemptAuthentication
方法(由 doFilter
方法調用),源碼如下:
@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 中獲取到的用戶名和密碼,構建瞭一個 UsernamePasswordAuthenticationToken
對象,我們可以看到這個構造方法的代碼,非常簡單:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super((Collection)null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); }
隻是保存瞭用戶名和密碼的引用,並且將認證狀態設置為 false
,因為此時隻是封裝瞭認證信息,還沒有進行認證。
我們再回到 attemptAuthentication
的代碼,在方法的最後一行,將創建好的認證信息,傳遞給瞭一個 AuthenticationManager
進行認證。這裡實際工作的是 AuthenticationManager
的實現類 ProviderManager
。
查找處理認證的 Provider 類
進入 ProviderManager
可以從源碼中找到 authenticate
方法,代碼比較長,我就不貼在這裡瞭,你可以自行查找,我簡述一下代碼中的邏輯。
ProviderManager
本身不執行認證操作,它管理著一個 AuthenticationProvider
列表,當需要對一個封裝好的認證信息進行認證操作的時候,它會將認證信息和它管理者的 Provider 們,逐一進行匹配,找到合適的 Provider 處理認證的具體工作。
可以這樣理解,ProviderManager
是一個管理者,管理著各種各樣的 Provider。當有工作要做的時候,它從來都不親自去做,而是把不同的工作,分配給不同的 Provider 去操作。
最後,它會將 Provider 的工作成果(已認證成功的信息)返回,或者拋出異常。
那麼,它是怎麼將一個認證信息交給合適的 Provider 的呢?
在上一部分中,我們說到,認證信息被封裝成瞭一個 UsernamePasswordAuthenticationToken
,它是Authentication
的子類,ProviderManager
會將這個認證信息的類型,傳遞個每個 Provider 的 supports
方法,由 Provider 來告訴 ProviderManager
它是不是支持這個類型的認證信息。
認證邏輯
在 Spring Security 內置的 Provider 中,與 UsernamePasswordAuthenticationToken
對應的 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
信息,這個方法的具體實現,可以在DaoAuthenticationProvider
中找到,主要是通過UserDetailsService
的loadUserByUsername
方法,查找系統中的用戶信息。 - 代碼第 25 行,通過
preAuthenticationChecks.check
方法,進行瞭認證前的一些校驗。校驗的具體實現可以在DefaultPreAuthenticationChecks
內部類中找到,主要是判斷用戶是否鎖定、是否可用、是否過期。 - 代碼第 26 行,通過
additionalAuthenticationChecks
方法,對用戶名和密碼進行瞭校驗。具體實現可以在DaoAuthenticationProvider
中找到。 - 代碼第 39 行,通過
postAuthenticationChecks.check
方法,校驗瞭密碼是否過期。具體實現可以在DefaultPostAuthenticationChecks
內部類中找到。 - 最後,如果以上校驗和認證都沒有問題,則通過
createSuccessAuthentication
方法,創建成功的認證信息,並返回。此時,就成功通過瞭認證。
在最後的 createSuccessAuthentication
方法中,會創建一個新的 UsernamePasswordAuthenticationToken
認證信息,這個新的認證信息的認證狀態為 true
。表示這是一個已經通過的認證。
這個認證信息會返回到 UsernamePasswordAuthenticationFilter
中,並作為 attemptAuthentication
方法的結果。
在 doFilter
方法中,會根據認證成功或失敗的結果,調用相應的 Handler 類進行後續的處理,最後,認證的信息也會被保存在 SecurityContext 中,供後續使用。
總結
到此這篇關於Spring Security認證流程的文章就介紹到這瞭,更多相關Spring Security認證流程內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Spring Security 實現用戶名密碼登錄流程源碼詳解
- Spring Security登陸流程講解
- Security 登錄認證流程詳細分析詳解
- Spring Security 實現多種登錄方式(常規方式外的郵件、手機驗證碼登錄)
- Spring Security中用JWT退出登錄時遇到的坑