Spring Security源碼解析一:UsernamePasswordAuthenticationFilter之登錄流程

一.前言

spring security安全框架作為spring系列組件中的一個,被廣泛的運用在各項目中,那麼spring security在程式中的工作流程是個什麼樣的呢,它是如何進行一系列的鑒權和認證呢,下面讓我們走進源碼,從源碼的角度來從頭走一遍spring security的工作流程。

二.spring security核心結構

當一個外部請求進入到我們應用中的時候,首先會通過我們的應用過濾器鏈ApplicationFilterChain,我們將遍歷該過濾器鏈中每一個Filter進行對應的處理,下面我們來看下ApplicationFiterChain一般情況下有哪些Filter

  
//ApplicationFilterChain遍歷內部Fiter進行doFilter()處理
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (this.pos < this.n) { ApplicationFilterConfig filterConfig = this.filters[this.pos++]; try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE); } if (Globals.IS_SECURITY_ENABLED) { Principal principal = ((HttpServletRequest)request).getUserPrincipal(); Object[] args = new Object[]{request, response, this}; SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal); } else { filter.doFilter(request, response, this); }

 

 

 

 從上面的圖中我們可以看到,chain在一般情況下中主要存在著這麼幾個filter,其中有我們比較熟悉的characterEncodeingFilter字元編碼的過濾器等等,以及我們本次內容的主角:springSecurityFiterChain spring security的過濾器鏈,可以說這就是spring security的核心所在,springSecurityFiterChain雖然為filter,但他在這裡實際扮演的是一個filterChain的角色,從他的的BeanName也可以看出,那我們接下來進入springSecurityFiterChain.doFilter()方法中,看看它內部又有哪些filter,以及內部的邏輯是怎樣的

 

 

 可以看到springSecurityFiterChain其實是個代理bean,它的doFilter()中實際用的delegate.doFilter(),delegate是個FilterChainProxy,下面來看下FiterChainProxy的內部實現。

FilterChainProxy.doFilter()方法內部邏輯

@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
//如果當前游標==additionalFilters的長度,即已經遍歷完該列表內的Filter,則結束FilterChainProxy.doFilter()
if (this.currentPosition == this.size) { if (logger.isDebugEnabled()) { logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest))); } // Deactivate path stripping as we exit the security filter chain this.firewalledRequest.reset(); this.originalChain.doFilter(request, response); return; }
this.currentPosition++;
//獲取列表中下一個Filter Filter nextFilter
= this.additionalFilters.get(this.currentPosition - 1); if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(), this.currentPosition, this.size)); }
//執行下一個Filter的doFilter()方法 nextFilter.doFilter(request, response,
this); } }

我們看到FilterChainProxy中維護了一個Filter列表 additionalFilters,doFilter()中會順序遍歷這個列表,執行每一個Filter的doFilters,那這個列表中具體有哪些Filter呢,讓我們來看一下

 

 

 

可以看到該列表中一共有13個spring security內置實現的Filter,系列文章中我們也主要來看SecurityContextPersistenceFilter security上下文持久化的過濾器,主要用來將認證過後的Authentication從session中提取注入本地執行緒變數中,以及UsernamePasswordAuthenticationFilter,用戶密碼認證過濾器,主要用來處理通過指定的登錄的POST方式的請求url來進行認證…..如果我們的項目中實現了JWT+Spring security的話,一般我們的我們會將自定義實現的JWT過濾器也加入到這條執行鏈中,並且執行位置放到UserNamePasswordAuthenticationFilter之前。

那麼Spring security的大體工作流程就如下圖:

 

 

 

三.UserNamePasswordAuthenticationFilter之登錄的認證流程

這裡我們並沒有按照上面的列表順序從頭開始講,第一個原因是本系列不會解析列表裡所有的過濾器,第二個則是個人覺得登錄是開啟security的入口,從登錄開始解析,之後再反過頭串聯前面的Filter,會有更好的效果。

 

通常情況下我們在security的配置中配置了哪些請求路徑是開放的,哪些路徑的需要許可權的,訪問了需要許可權的請求時,如果沒有許可權便會跳轉到security默認的登錄頁中,這時候我們可以進行輸入帳號密碼進行登錄,那這次登錄請求security是如何處理的呢,讓我們來看看UserNamePasswordAuthenticationFilter.doFilter()方法

 

//UserNamePasswordAuthenticationFilter本身並沒有實現doFilter()方法,使用的是其父類的doFilter()方法
public
class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { } public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); } private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//判斷當前請求是否滿足認證條件,不滿足則結束流程,進行下一個過濾器的工作,如何判斷滿不滿足認證條件的邏輯,在下面進行解析
if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try {
//當前請求滿足認證條件,開始嘗試去認證,attemptAuthentication()由UserNamePasswordAuthencationFilter自己實現 Authentication authenticationResult
= attemptAuthentication(request, response);
//如果沒有一個認證管理器能認證,結果為null,則直接結束
if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; }
//調用session策略將認證通過的憑證儲存在Session中,保證登陸之後後續同瀏覽器登陸不需要再登錄,這個詳細邏輯等到後續展開講解
this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); }
//認證通過後的處理,這裡會將當前的憑證填充到本地執行緒變數中,以及會如何在security的配置中配置了successForwardUrl,將會跳轉至該URL successfulAuthentication(request, response, chain, authenticationResult); }
catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { // Authentication failed unsuccessfulAuthentication(request, response, ex); } } }

 

requiresAuthentication()判斷請求是否滿足認證條件

    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
//內部調用了成員屬性中的RequestMatcher去進行匹配,下面邏輯可以看到只有請求是POST的並且路徑跟pattern完全匹配才會返回true
if (this.requiresAuthenticationRequestMatcher.matches(request)) { return true; } if (this.logger.isTraceEnabled()) { this.logger .trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher)); } return false; }

我們來看下UserNamePasswordAuthencationFilter內置的AntPathRequestMatcher的屬性以及匹配邏輯

 

 

 可以看到該AntPathRequestMatcher有兩個重要的屬性,pattern=”/login”, httpMethod=”POST”,這兩個屬性將在matchs()方法中起到決定性作用。

    
public boolean matches(HttpServletRequest request) {
//如果請求方式不是httpMthod不一致,也就是非POST請求,則不匹配,返回false
if (this.httpMethod != null && StringUtils.hasText(request.getMethod()) && this.httpMethod != HttpMethod.resolve(request.getMethod())) { return false; }
//如果當前的pattern=/**,即任意請求,那麼匹配,返回true
if (this.pattern.equals(MATCH_ALL)) { return true; }
//獲取當前請求的路徑(去除掉項目根路徑) String url
= getRequestPath(request);
//通過debug源碼,這裡採取的是完全匹配,即需要請求路徑和pattern(/login)完全一致才返回true,當然這個pattern是可以配置的,之後會詳細指出
return this.matcher.matches(url); }

 

來看一下UserNamePasswordAuthenticationFilter實現的attemptAuthentication()方法

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
//判斷security當前是否只支援POST請求
if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); }
//從請求中獲取當前登錄的用戶名,即從request.getParamter("username")中獲取 String username
= obtainUsername(request);
//沒有該參數的話默認空串 username
= (username != null) ? username : "";
//去除空格 username
= username.trim();
//獲取密碼,即request.getPassword("password") String password
= obtainPassword(request); password = (password != null) ? password : "";
//根據用戶密碼初始化Authentication憑證,具體為UsernamePasswordAuthentication這種類型的憑證 UsernamePasswordAuthenticationToken authRequest
= new UsernamePasswordAuthenticationToken(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest);
//調用當前環境的認證管理器進行認證, 這裡內置的是ProviderManager
return this.getAuthenticationManager().authenticate(authRequest); }

 

ProviderManager.authenticate()認證方法

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//獲取傳入參數中的認證憑證的類型,當前為UsernamePasswordAuthenticationToken Class
<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; int size = this.providers.size();
//遍歷該認證管理器下所有的授權者(或叫認證者)
for (AuthenticationProvider provider : getProviders()) {
//判斷當前授權者是否支援對傳入的這種類型的憑證進行認證授權
if (!provider.supports(toTest)) { continue; } if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)", provider.getClass().getSimpleName(), ++currentPosition, size)); } try {
//當前授權者對該憑證進行認證授權,並將結果保存在result中 result
= provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException ex) { prepareException(ex, authentication); throw ex; } catch (AuthenticationException ex) { lastException = ex; } }
//如果遍歷完所有的授權者都不能認證,並且當前認證管理器存在父親認證管理器,那麼就調用他父親認證管理器重複上述操作
if (result == null && this.parent != null) { try {
//保存父親認證管理器的認證結果(即授權通過的憑證) parentResult
= this.parent.authenticate(authentication);
//賦值到當前的result result
= parentResult; } catch (ProviderNotFoundException ex) { } catch (AuthenticationException ex) { parentException = ex; lastException = ex; } } if (result != null) {
//身份驗證已完成。刪除憑據和其他機密數據
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { ((CredentialsContainer) result).eraseCredentials(); }

                //如果嘗試了父級AuthenticationManager並成功,則
                //將發布AuthenticationSuccessEvent
                //如果父級驗證失敗,此檢查將防止重複AuthenticationSuccessEvent
                //AuthenticationManager已經發布了它

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }
//返回認證結果
return result; } if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } if (parentException == null) { prepareException(lastException, authentication); } throw lastException; }

 

可以看到當前ProviderManager下只有一個授權者,並且該授權者也不支援對UsernamePasswordAuthenticationToken這種憑證進行授權,所以需要調用該ProviderManager的父親ProviderManager進行嘗試認證

 

 

 

 來看下它的父親ProviderManager有哪些授權者,是否能夠對UsernamePasswordAuthenticationToken進行認證授權

 

 

 可以看到這個父親認證管理器中也只有一個授權者,DaoAuthenticationProvider,利用資料庫數據進行認證授權的,而這個就是能夠支援對UsernamePasswordAuthenticationToken這種憑證進行認證授權的,下面我們就來看下DaoAuthenticationProvider是如何認證授權的

 

DaoAuthenticationProvider.authenticate()方法,該方法由它的父類AbstractUserDetailsAuthenticationProvider實現

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//該授權者只支援對UsernamePasswordAuthenticationToken這種類型憑證進行認證授權,不是的話即拋出異常 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 {
//快取中沒有,則直接根據username從資料庫中查,這裡用的其實就是我們自己實現的UserDetailsService.loadUserByUsername(username)方法,下面會展示程式碼 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 {
//對查詢出來的用戶資訊進行預校驗,主要檢測的就是UserDetails實體中的
isAccountNonExpired是否過期,isAccountNonLocked是否被鎖定,
isEnabled是否可用,只要有一個不滿足就拋出異常
       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);
}
//預檢通過,進行後置檢查,這裡主要檢查UserDeatils中的isCredentialNonExpired, 即檢查密碼是否過期,同樣由我們自定義的UserDetailsService邏輯的來判斷,是否過期
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
//不是從快取中拿的就存到快取中
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//檢驗通過,將傳入的憑證構造成認證通過的憑證
return createSuccessAuthentication(principalToReturn, authentication, user);
}

 

DaoAuthenticationProvider.retrieveUser()從資料庫中獲取用戶資訊

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
//通常我們使用security 都會自己實現UserDetailService進行配置,這裡就用到了,它會通過我們自定義的邏輯來找尋用戶並返回用戶資訊 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) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }

 

AbstractUserDetailsAuthenticationProvider的內部類實現的check方法()

    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {

        @Override
        public void check(UserDetails user) {
//校驗是否被鎖定
if (!user.isAccountNonLocked()) { AbstractUserDetailsAuthenticationProvider.this.logger .debug("Failed to authenticate since user account is locked"); throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked")); }
//校驗是否可用
if (!user.isEnabled()) { AbstractUserDetailsAuthenticationProvider.this.logger .debug("Failed to authenticate since user account is disabled"); throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled")); }
//校驗是否過期
if (!user.isAccountNonExpired()) { AbstractUserDetailsAuthenticationProvider.this.logger .debug("Failed to authenticate since user account has expired"); throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired")); } } }

 

DaoAauthenticationProvider.additionalAuthenticationChecks()對憑證進行附加資訊的校驗,主要是校驗密碼

    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//如果憑證中的密碼==null則直接拋出異常
if (authentication.getCredentials() == null) { this.logger.debug("Failed to authenticate since no credentials provided"); throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); }
//從憑證中獲取當前登錄的密碼 String presentedPassword
= authentication.getCredentials().toString();
//調用security環境中的PasswordEncoder將憑證中的密碼與資料庫查詢出的密碼進行匹配,匹配不成功則拋出異常,
這個PasswordEncoder通常我們在使用security的
時候都會替換成自定義的Encoder,根據自己項目的需求進行自定義的實現其中的邏輯
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this.logger.debug("Failed to authenticate since password does not match stored value"); throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }

 

AbstractAuthenticationProcessingFilter.successfulAuthentication()方法:認證通過後一些收尾處理

    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException {
//創建一個新的security上下文 SecurityContext context
= SecurityContextHolder.createEmptyContext();
//將憑證填充進上下文 context.setAuthentication(authResult);
//將上下文保存到本地執行緒變數中 SecurityContextHolder.setContext(context);
if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); }
//security 開啟了remember me功能的話的處理
this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); }
//security中配置了successForwardUrl的話,這裡登錄成功後就會跳轉到指定url上
this.successHandler.onAuthenticationSuccess(request, response, authResult); }

 

四:最後

這篇文章是spring security源碼解析的第一章,主要是解析了下security 是如何處理登錄流程的,主要就是來看UsernamePasswordAuthenticationFilter的內部處理邏輯,通過源碼我們也發現,默認情況下只有你的請求的POST方式的 /login,security才會認為這是登錄請求,才會讓該請求走UsernamePasswordAuthenticationFilter的處理邏輯,當然這個路徑也是可以配置的,如下程式碼,配置了loginProcessingUrl的路徑之後,再次登錄,security就會以該路徑當做登錄請求。

    protected void configure(HttpSecurity http) throws Exception {
        http.csrf()
                .disable() 
                .logout()
                .and()
                .formLogin()  //該路徑也是UserNamePasswordAuthenticationFilter用來識別當前請求是不是登錄請求,是的話才進行登錄處理
                .loginProcessingUrl("/loginMy") 
/...略../ }

 

關於UsernamePasswordAuthenticationFilter的內容暫時告一段落,下篇內容將繼續通過源碼解析剩下的過濾器SecurityContextPersistenceFilter security上下文持久化的過濾器等等….。

由於筆者水平有限,有些地方可能講解的有錯誤,希望大家能夠幫忙指出,共同進步。