SpringSecurity認(rèn)證流程分析

1. 前言

SpringSecurity的認(rèn)證,其實就是我們的登錄驗證。

Web系統(tǒng)中登錄驗證的核心就是**憑證**,比較多使用的是`Session`和`JWT`,其原理都是在用戶成功登錄后返回給用戶一個憑證,后續(xù)用戶訪問時需要攜帶憑證來辨別自己的身份。后端會根據(jù)這個憑證進(jìn)行安全判斷,如果憑證沒問題則代表已登錄,否則則直接拒絕請求。

以下內(nèi)容會先分析源碼理清楚認(rèn)證的流程。

2. SpringSecurity的工作流程

要想分析SpringSecurity的認(rèn)證流程,就一定要先了解整個SpringSecurity的工作流程,我們才能最終進(jìn)行一些自定義操作。

Spring Security的web基礎(chǔ)是Filters(過濾器),它是通過一層層的Filters來對web請求做處理的,每一個web請求會經(jīng)過一條過濾器鏈,在這個過程中完成認(rèn)證與授權(quán)。

其具體工作流程是這樣的:

  1. 在Spring的過濾器鏈中,Spring Security向其添加了一個FilterChainProxy過濾器,而這個過濾器只是一個代理過濾器,通過這個代理過濾器創(chuàng)建一套SpringSecurity自定義的過濾器鏈(認(rèn)證與授權(quán)過濾器就在這過濾器鏈中),然后再執(zhí)行這一系列自定義的過濾器。如圖所示(網(wǎng)上找的)

    authentication_0.jpg
  2. 然后我們可以來看看這個代理過濾器FilterChainProxy的部分源碼:

    FilterChainProxy.jpg

    下面debug程序,設(shè)置斷點,看看過濾器鏈中有哪些過濾器

    getFilters.jpg

    這些過濾器中,我們重點關(guān)注UsernamePasswordAuthenticationFilterFilterSecurityInterceptor,其中UsernamePasswordAuthenticationFilter負(fù)責(zé)登錄認(rèn)證,FilterSecurityInterceptor負(fù)責(zé)授權(quán)。

  3. SpringSecurity的基本原理(網(wǎng)絡(luò)上找的一張圖)

    authentication_1.jpeg

    如圖所示,一個請求想要訪問到API就會從左到右經(jīng)過藍(lán)線框里的過濾器,其中綠色部分是負(fù)責(zé)認(rèn)證的過濾器,藍(lán)色部分是負(fù)責(zé)異常處理,橙色部分則是負(fù)責(zé)授權(quán),對應(yīng)了我們代碼debug的過濾器鏈。

    注意:只有在配置中打開了formLogin配置項,過濾器鏈中才會加入它們,否則是不會被加到過濾器鏈的。

    SpringSecurity中有兩個配置項叫formLoginhttpBasic,分別對應(yīng)著表單認(rèn)證方式(過濾器是UsernamePasswordAuthenticationFilter)和Basic認(rèn)證方式(過濾器是BasicAuthenticationFilter),分別對應(yīng)上圖

3. SpringSecurity中的重要組件

  • Authentication:認(rèn)證接口,存儲了認(rèn)證信息,代表當(dāng)前登錄用戶

  • SecurityContext:上下文對象,Authentication對象會放在里面

  • SecurityContextHolder:用于拿到上下文對象的靜態(tài)工具類

  • AuthenticationManager:用于校驗Authentication,返回一個認(rèn)證完成后的Authentication對象

  1. 我們需要通過SecurityContext上下文對象來獲取認(rèn)證對象Authentication,而SecurityContext又是交給SecurityContextHolder進(jìn)行管理的。

  2. 查看源碼

    • SecurityContext

      SecurityContext.jpg

      接口只有兩個方法,作用就是get/set Authentication

    • SecurityContextHolder

      SecurityContextHolder.jpg

      可以人為這是SecurityContext的一個靜態(tài)工具類,主要有g(shù)et,set,clear處理SecurityContext,其原理是使用ThreadLocal來保證一個線程中傳遞同一個對象

  3. 我們可以通過以下代碼在程序任何地方獲取Authentication

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    
  4. 再看看AuthenticationAuthenticationManager源碼

    Authentication: (注釋太長不好截圖)

    public interface Authentication extends Principal, Serializable {
         Collection<? extends GrantedAuthority> getAuthorities();
        
         Object getCredentials();
        
         Object getDetails();
        
         Object getPrincipal();
        
         boolean isAuthenticated();
        
         void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
    }
    
    

    這幾個方法的作用如下:

    • getAuthorities:獲取用戶權(quán)限(角色信息)
    • getCredentials:獲取證明用戶認(rèn)證的信息,一般是指密碼等信息
    • getDetails:獲取用戶額外的信息
    • getPrincipal:獲取用戶身份信息,在未認(rèn)證的時候獲取的是用戶名,在已認(rèn)證后獲取的是UserDetails對象
    • isAuthenticated:獲取當(dāng)前Authentication是否已認(rèn)證
    • setAuthenticated:設(shè)置當(dāng)前Authentication是否已認(rèn)證(true/false)

    AuthenticationManager

    AuthenticationManager.jpg

    該接口定義了一個認(rèn)證方法,將一個未被認(rèn)證的Authentication傳入,返回一個已認(rèn)證的Authentication

  5. 總結(jié)下SpringSecurity的認(rèn)證流程

    將上面四個組件串聯(lián)起來,可以大致了解到認(rèn)證的流程:

    1. 一個請求帶著身份信息進(jìn)來
    2. 經(jīng)過AuthenticationManager進(jìn)行認(rèn)證
    3. 然后通過SecurityContextHolder獲取到SecurityContext
    4. 最后將認(rèn)證后的Authentication放入SecurityContext,這樣下一個請求進(jìn)來就能知道是否已認(rèn)證過

4. 完整源碼流程

有了以上的一些基礎(chǔ)了解后,我們來順著源碼流程走一邊,理清整個認(rèn)證的流程。

基于formLogin的流程分析,SpringSecurity默認(rèn)也是formLogin。

以下源碼我都將注釋去掉,否則太長了!

  1. 第一步:請求進(jìn)來,到達(dá)UsernamePasswordAuthenticationFilter過濾器

    public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
     public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    
     public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    
     private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
             "POST");
    
     private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    
     private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    
     private boolean postOnly = true;
    
     public UsernamePasswordAuthenticationFilter() {
         super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
     }
    
     public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
         super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
     }
    
     @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);
     }
    
     @Nullable
     protected String obtainPassword(HttpServletRequest request) {
         return request.getParameter(this.passwordParameter);
     }
    
     @Nullable
     protected String obtainUsername(HttpServletRequest request) {
         return request.getParameter(this.usernameParameter);
     }
    
     protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
         authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
     }
    
     public void setUsernameParameter(String usernameParameter) {
         Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
         this.usernameParameter = usernameParameter;
     }
    
     public void setPasswordParameter(String passwordParameter) {
         Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
         this.passwordParameter = passwordParameter;
     }
    
     public void setPostOnly(boolean postOnly) {
         this.postOnly = postOnly;
     }
    
     public final String getUsernameParameter() {
         return this.usernameParameter;
     }
    
     public final String getPasswordParameter() {
         return this.passwordParameter;
     }
    
    }
    

    分析:

    1. 過濾器中定義了一些默認(rèn)的信息,比如默認(rèn)用戶名參數(shù)為username,密碼參數(shù)為password,默認(rèn)請求為/login,但同時也提供了set、get方法讓我們自定義,自定義的方式就是在配置類WebSecurityConfigurerAdapter的子類中重寫configure(HttpSecurity http)設(shè)置

    2. 過濾器的處理核心就是doFilter,但我們在UsernamePasswordAuthenticationFilter中并沒有看到,這是因為在他父類AbstractAuthenticationProcessingFilter實現(xiàn)了。

    3. 進(jìn)入AbstractAuthenticationProcessingFilter查看

      @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 {
          // 先通過請求的uri判斷是否需要認(rèn)證,比如默認(rèn)的/login就不需要認(rèn)證了
          if (!requiresAuthentication(request, response)) {
              chain.doFilter(request, response);
              return;
          }
          try {
              Authentication authenticationResult = attemptAuthentication(request, response);
              if (authenticationResult == null) {
                  // return immediately as subclass has indicated that it hasn't completed
                  return;
              }
              this.sessionStrategy.onAuthentication(authenticationResult, request, response);
              // Authentication success
              if (this.continueChainBeforeSuccessfulAuthentication) {
                  chain.doFilter(request, response);
              }
              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);
          }
      }
      
      protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authResult) throws IOException, ServletException {
          SecurityContext context = SecurityContextHolder.createEmptyContext();
          context.setAuthentication(authResult);
          SecurityContextHolder.setContext(context);
          if (this.logger.isDebugEnabled()) {
              this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
          }
          this.rememberMeServices.loginSuccess(request, response, authResult);
          if (this.eventPublisher != null) {
              this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
          }
          this.successHandler.onAuthenticationSuccess(request, response, authResult);
      }
      

      分析:

      1. doFilter首先判斷uri是否需要認(rèn)證
      2. 接著執(zhí)行方法Authentication authenticationResult = attemptAuthentication(request, response);進(jìn)行認(rèn)證,從函數(shù)名也能看出是嘗試認(rèn)證,認(rèn)證成功獲取認(rèn)證對象Authentication,這是這個過濾器的核心
      3. 認(rèn)證成功,則執(zhí)行successfulAuthentication(),將已認(rèn)證的Authentication存放到SecurityContext,認(rèn)證失敗則通過認(rèn)證失敗處理器AuthenticationFailureHandler處理
      4. 接下來研究下attemptAuthentication方法,這個方法在當(dāng)前父類中是一個抽象方法,由子類實現(xiàn),而AbstractAuthenticationProcessingFilter的一個子類就是UsernamepasswordAuthenticationFilter,回到這個類看看這個attemptAuthentication方法
    4. 分析UsernamepasswordAuthenticationFilterattemptAuthentication方法

      @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);
      }
      

      分析:

      1. 從源碼可以看出,首先如果不是POST請求,直接拋出異常

      2. 然后從當(dāng)前請求中獲取用戶名username和密碼password

      3. 通過當(dāng)前用戶名和密碼,構(gòu)造一個令牌UsernamePasswordAuthenticationToken

        這個UsernamePasswordAuthenticationToken繼承了AbstractAuthenticationToken,而AbstractAuthenticationToken又實現(xiàn)了Authentication接口,所以實質(zhì)上這個token就是一個Authentication對象

      4. 最后調(diào)用this.getAuthenticationManager().authenticate(authRequest)返回,這里就用到了AuthenticationManager去認(rèn)證了,這個稍后在看

    5. 接著看這個方法this.getAuthenticationManager().authenticate(authRequest)

      這里使用的是AuthenticationManager接口的方法去進(jìn)行認(rèn)證,這個方法authenticate很奇特,傳入的參數(shù)和返回值類型都是Authentication.

      public interface AuthenticationManager {
        Authentication authenticate(Authentication authentication) throws AuthenticationException;
      }
      

      該接口方法的作用是:**對用戶未認(rèn)證的憑據(jù)進(jìn)行認(rèn)證,認(rèn)證通過后返回已認(rèn)證的憑據(jù),否則拋出認(rèn)證異常AuthenticationException

      分析:

      從源碼可以看到,這個AuthenticationManager是一個接口,所以他并不是真正做事情的那個,只是提供了一個標(biāo)準(zhǔn),真正實現(xiàn)功能的是它的子類。

      通過(ctrl + h)查看AuthenticationManager接口的實現(xiàn)類,可以看到如下:

      AuthenticationManager-impl.jpg

      其他幾個都是內(nèi)部類,所以我們找到了ProviderManager實現(xiàn)了AuthenticationManager

      我們看看他實現(xiàn)的authenticate方法:

      @Override
      public Authentication authenticate(Authentication authentication) throws AuthenticationException {
          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();
          // 遍歷AuthenticationProvider,列表中的每個Provider依次進(jìn)行認(rèn)證
          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 = provider.authenticate(authentication);
                  if (result != null) {
                      copyDetails(authentication, result);
                      break;
                  }
              }
              catch (AccountStatusException | InternalAuthenticationServiceException ex) {
                  prepareException(ex, authentication);
                  // SEC-546: Avoid polling additional providers if auth failure is due to
                  // invalid account status
                  throw ex;
              }
              catch (AuthenticationException ex) {
                  lastException = ex;
              }
          }
          // 如果 AuthenticationProvider 列表中的Provider都認(rèn)證失敗,且之前有構(gòu)造一個 AuthenticationManager 實現(xiàn)類,那么利用AuthenticationManager 實現(xiàn)類 繼續(xù)認(rèn)證
          if (result == null && this.parent != null) {
              // Allow the parent to try.
              try {
                  parentResult = this.parent.authenticate(authentication);
                  result = parentResult;
              }
              catch (ProviderNotFoundException ex) {
                 
              }
              catch (AuthenticationException ex) {
                  parentException = ex;
                  lastException = ex;
              }
          }
          // 認(rèn)證成功
          if (result != null) {
              if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
                  // Authentication is complete. Remove credentials and other secret data
                  // from authentication
                  // 認(rèn)證成功后刪除驗證信息
                  ((CredentialsContainer) result).eraseCredentials();
              }
              // If the parent AuthenticationManager was attempted and successful then it
              // will publish an AuthenticationSuccessEvent
              // This check prevents a duplicate AuthenticationSuccessEvent if the parent
              // AuthenticationManager already published it
              // 發(fā)布登錄成功事件
              if (parentResult == null) {
                  this.eventPublisher.publishAuthenticationSuccess(result);
              }
      
              return result;
          }
      
          // Parent was null, or didn't authenticate (or throw an exception).
          if (lastException == null) {
              lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
                                                                                     new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
          }
          // If the parent AuthenticationManager was attempted and failed then it will
          // publish an AbstractAuthenticationFailureEvent
          // This check prevents a duplicate AbstractAuthenticationFailureEvent if the
          // parent AuthenticationManager already published it
          if (parentException == null) {
              prepareException(lastException, authentication);
          }
          throw lastException;
      }
      

      分析:

      從源碼中看出,ProviderManager并不是自己直接對請求進(jìn)行驗證,而是循環(huán)一個AuthenticationProvider列表,列表中每一個provider依次進(jìn)行判斷是否使用它進(jìn)行驗證。

    6. 接下來看看AuthenticationProvider

      這個AuthenticationProvider也是一個接口

      public interface AuthenticationProvider {
          // 認(rèn)證方法
        Authentication authenticate(Authentication authentication) throws AuthenticationException;
        // 該Provider是否支持對應(yīng)的Authentication類型
        boolean supports(Class<?> authentication);
      }
      

      同樣看看這個接口有哪些實現(xiàn)類:

      AuthenticationProvider.jpg

      這個接口的實現(xiàn)類和繼承類有很多,我們直接看與User相關(guān)的,會看到有一個AbstractUserDetailsAuthenticationProvider抽象類,他的實現(xiàn)類是DaoAuthenticationProvider, 才是真正做驗證的人

      authenticate是由AbstractUserDetailsAuthenticationProvider`實現(xiàn)的,源碼如下:

      @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 {
                  // retrieveUser是一個抽象方法,子類中實現(xiàn)
                  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;
              // retrieveUser是一個抽象方法,子類中實現(xiàn)
              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();
          }
          // 創(chuàng)建一個成功的Authentication對象返回
          return createSuccessAuthentication(principalToReturn, authentication, user);
      }
      

      在這個authenticate方法里,真正做驗證的方法是:retrieveUser,該方法是在子類DaoAuthenticationProvider中實現(xiàn)的

      @Override
      protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
          throws AuthenticationException {
          prepareTimingAttackProtection();
          try {
              // 通過loadUserByUsername獲取用戶信息,返回一個UserDetails
              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);
          }
      }
      // 重寫了父類的方法,對密碼進(jìn)行一些加密操作
      @Override
      protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
                                                           UserDetails user) {
          boolean upgradeEncoding = this.userDetailsPasswordService != null
              && this.passwordEncoder.upgradeEncoding(user.getPassword());
          if (upgradeEncoding) {
              String presentedPassword = authentication.getCredentials().toString();
              String newPassword = this.passwordEncoder.encode(presentedPassword);
              user = this.userDetailsPasswordService.updatePassword(user, newPassword);
          }
          return super.createSuccessAuthentication(principal, authentication, user);
      }
      

      分析:

      這個retrieveUser方法,就是調(diào)用UserDetailsServiceloadUserByUsername方法,這個UserDetailsService就是一個服務(wù)接口,加載UserDetails,一般是從數(shù)據(jù)庫中去查找用戶,封裝為UserDetails對象返回,找不到就報異常。

      SpringSecurity默認(rèn)實現(xiàn)了一個UserDetails的實現(xiàn)類User,當(dāng)我們使用將用戶信息存儲在內(nèi)存的方式auth.inMemoryAuthentication()時,會創(chuàng)建一個InMemoryUserDetailsManager,這個類創(chuàng)建了一個UserDetails的實現(xiàn)類User,

      同時這個類實現(xiàn)了UserDetailsManager接口,而UserDEtailsManager又是繼承自UserDetailsService,所以默認(rèn)情況下的話就是調(diào)用InMemoryUserDetailsManager類的loadUserByUsername方法

      因此,當(dāng)我們需要自定義時,則需要自己實現(xiàn)UserDetailsService接口和UserDetails接口

    7. UserDetailsServiceUserDetails接口

      UserDetailsService就是定義了一個加載UserDetails的接口,通常我們會實現(xiàn)這個接口,然后從數(shù)據(jù)庫中查詢相關(guān)用戶信息,再返回。

      public interface UserDetailsService {
        UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
      
      }
      

      UserDetails也是一個接口,在實際開發(fā)中也會對他進(jìn)行定制化,提供核心用戶信息。

      SpringSecurity處于安全考慮,UserDetails只是存儲用戶信息,這些信息最后會封裝到Authentication對象中的。

      public interface UserDetails extends Serializable {
        // 返回用戶的權(quán)限集合
        Collection<? extends GrantedAuthority> getAuthorities();
      
        /**
         * Returns the password used to authenticate the user.
         * @return the password
         */
        String getPassword();
      
        /**
         * Returns the username used to authenticate the user. Cannot return
         * <code>null</code>.
         * @return the username (never <code>null</code>)
         */
        String getUsername();
      
        // 用戶賬戶是否過期
        boolean isAccountNonExpired();
      
        // 用戶是否被鎖定
        boolean isAccountNonLocked();
      
        // 用戶的密碼是否已過期
        boolean isCredentialsNonExpired();
      
        // 用戶是否被禁用
        boolean isEnabled();
      
      }
      
    8. 至此,整個認(rèn)證流程差不多就走完了,這個過程中,梳理以下,我們是以默認(rèn)的登錄方式來分析流程的,默認(rèn)的登錄方式用到的是:UsernamePasswordAuthenticationFilterUsernamePasswordAuthenticationToken以及DaoAuthenticationProvider這些來進(jìn)行身份的驗證,那么以后我們要添加別的驗證方式的話,就可以模仿這個流程:重新繼承AbstractAuthenticationProcessingFilter,AbstractAuthenticationToken,AuthenticationProvider。

      流程圖大致如下:

      AuthenticationProvider-1.jpg
  2. 返回過程

    1. DaoAuthenticationProvider類的retrieveUser方法通過loadUserByUsername獲取到用戶信息后返回一個UserDetails對象給到父類AbstractUserDetailsAuthenticationProvider的方法authenticate

    2. AbstractUserDetailsAuthenticationProvider拿到返回的UserDetails后,調(diào)用了return createSuccessAuthentication(principalToReturn, authentication, user);創(chuàng)建了一個可信的UsernamepasswordAuthenticationToken,并返回給了ProviderManagerauthenticate方法

    3. 這時候的UsernamepasswordAuthenticationToken是已驗證過的可信的,再往上返回AuthenticationUsernamepasswordAuthenticationToken是他的一個實現(xiàn)類,多態(tài))

    4. 再回到了UsernamepasswordAuthenticationFilter類的attemptAuthentication方法中,return this.getAuthenticationManager().authenticate(authRequest)返回到了AbstractAuthenticationProcessingFilter類中doFilter,最后調(diào)用了successfulAuthentication,將可信的Authentication對象保存到SecurityContext中,然后放行。

    5. 認(rèn)證成功Handler與失敗Handler

      UsernamepasswordAuthenticationFilter的父類AbstractAuthenticationProcessingFilter類的doFilter方法中,認(rèn)證成功會調(diào)用successfulAuthentication,失敗調(diào)用unsuccessfulAuthentication。

      AbstractAuthenticationProcessingFilter中對successfulAuthentication方法和unsuccessfulAuthentication做了默認(rèn)實現(xiàn),源碼如下:

      protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
          SecurityContext context = SecurityContextHolder.createEmptyContext();
          context.setAuthentication(authResult);
          SecurityContextHolder.setContext(context);
          if (this.logger.isDebugEnabled()) {
              this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
          }
          this.rememberMeServices.loginSuccess(request, response, authResult);
          if (this.eventPublisher != null) {
              this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
          }
          this.successHandler.onAuthenticationSuccess(request, response, authResult);
      }
      
      protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
          SecurityContextHolder.clearContext();
          this.logger.trace("Failed to process authentication request", failed);
          this.logger.trace("Cleared SecurityContextHolder");
          this.logger.trace("Handling authentication failure");
          this.rememberMeServices.loginFail(request, response);
          this.failureHandler.onAuthenticationFailure(request, response, failed);
      }
      

      分析:

      1. 方法最后分別對認(rèn)證成功和失敗做了自定義處理,最后分別調(diào)用了兩個Handler處理,AuthenticationSuccessHandler類型的successHandlerAuthenticationFailureHandler類型的failureHandler。
      2. AbstractAuthenticationProcessingFilter類中還分別提供了setAuthenticationSuccessHandler方法和setAuthenticationFailureHandler能讓我們實現(xiàn)自定義Handler的注入。
      3. 綜上分析,我們要自定義Handler就有兩種方式了
        • 自定義AuthenticationSuccessHandler類和AuthenticationFailureHandler分別實現(xiàn)onAuthenticationSuccess方法和onAuthenticationFailure方法,然后調(diào)用提供的set方法注入到類中
        • AbstractAuthenticationProcessingFilter的子類中去重寫successfulAuthentication方法和unsuccessfulAuthentication方法。

5. 整體流程圖

flow.jpg

6. 學(xué)習(xí)博客

【項目實踐】一文帶你搞定前后端分離下的認(rèn)證和授權(quán)|Spring Security + JWT

SpringSecurity+JWT認(rèn)證流程解析 | 掘金新人第一彈

SpringSecurit(小胖哥)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容