其他分享
首页 > 其他分享> > SpringSecurity认证流程分析

SpringSecurity认证流程分析

作者:互联网

1. 前言

​ SpringSecurity的认证,其实就是我们的登录验证。

​ Web系统中登录验证的核心就是凭证,比较多使用的是SessionJWT,其原理都是在用户成功登录后返回给用户一个凭证,后续用户访问时需要携带凭证来辨别自己的身份。后端会根据这个凭证进行安全判断,如果凭证没问题则代表已登录,否则则直接拒绝请求。

​ 以下内容会先分析源码理清楚认证的流程。

2. SpringSecurity的工作流程

要想分析SpringSecurity的认证流程,就一定要先了解整个SpringSecurity的工作流程,我们才能最终进行一些自定义操作。

Spring Security的web基础是Filters(过滤器),它是通过一层层的Filters来对web请求做处理的,每一个web请求会经过一条过滤器链,在这个过程中完成认证与授权。

其具体工作流程是这样的:

  1. 在Spring的过滤器链中,Spring Security向其添加了一个FilterChainProxy过滤器,而这个过滤器只是一个代理过滤器,通过这个代理过滤器创建一套SpringSecurity自定义的过滤器链(认证与授权过滤器就在这过滤器链中),然后再执行这一系列自定义的过滤器。如图所示(网上找的)

    authentication_0.jpg

  2. 然后我们可以来看看这个代理过滤器FilterChainProxy的部分源码:

    FilterChainProxy.jpg

    下面debug程序,设置断点,看看过滤器链中有哪些过滤器

    getFilters.jpg

    这些过滤器中,我们重点关注UsernamePasswordAuthenticationFilterFilterSecurityInterceptor,其中UsernamePasswordAuthenticationFilter负责登录认证,FilterSecurityInterceptor负责授权。

  3. SpringSecurity的基本原理(网络上找的一张图)

    authentication_1.jpeg

    如图所示,一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权,对应了我们代码debug的过滤器链。

    注意:只有在配置中打开了formLogin配置项,过滤器链中才会加入它们,否则是不会被加到过滤器链的。

    SpringSecurity中有两个配置项叫formLoginhttpBasic,分别对应着表单认证方式(过滤器是UsernamePasswordAuthenticationFilter)和Basic认证方式(过滤器是BasicAuthenticationFilter),分别对应上图

3. SpringSecurity中的重要组件

  1. 我们需要通过SecurityContext上下文对象来获取认证对象Authentication,而SecurityContext又是交给SecurityContextHolder进行管理的。

  2. 查看源码

    • SecurityContext

      SecurityContext.jpg

      接口只有两个方法,作用就是get/set Authentication

    • SecurityContextHolder

      SecurityContextHolder.jpg

      可以人为这是SecurityContext的一个静态工具类,主要有get,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:获取用户权限(角色信息)
    • getCredentials:获取证明用户认证的信息,一般是指密码等信息
    • getDetails:获取用户额外的信息
    • getPrincipal:获取用户身份信息,在未认证的时候获取的是用户名,在已认证后获取的是UserDetails对象
    • isAuthenticated:获取当前Authentication是否已认证
    • setAuthenticated:设置当前Authentication是否已认证(true/false)

    AuthenticationManager

    AuthenticationManager.jpg

    该接口定义了一个认证方法,将一个未被认证的Authentication传入,返回一个已认证的Authentication

  5. 总结下SpringSecurity的认证流程

    将上面四个组件串联起来,可以大致了解到认证的流程:

    1. 一个请求带着身份信息进来
    2. 经过AuthenticationManager进行认证
    3. 然后通过SecurityContextHolder获取到SecurityContext
    4. 最后将认证后的Authentication放入SecurityContext,这样下一个请求进来就能知道是否已认证过

4. 完整源码流程

有了以上的一些基础了解后,我们来顺着源码流程走一边,理清整个认证的流程。

基于formLogin的流程分析,SpringSecurity默认也是formLogin。

以下源码我都将注释去掉,否则太长了!

  1. 第一步:请求进来,到达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. 过滤器中定义了一些默认的信息,比如默认用户名参数为username,密码参数为password,默认请求为/login,但同时也提供了set、get方法让我们自定义,自定义的方式就是在配置类WebSecurityConfigurerAdapter的子类中重写configure(HttpSecurity http)设置

    2. 过滤器的处理核心就是doFilter,但我们在UsernamePasswordAuthenticationFilter中并没有看到,这是因为在他父类AbstractAuthenticationProcessingFilter实现了。

    3. 进入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判断是否需要认证,比如默认的/login就不需要认证了
          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是否需要认证
      2. 接着执行方法Authentication authenticationResult = attemptAuthentication(request, response);进行认证,从函数名也能看出是尝试认证,认证成功获取认证对象Authentication,这是这个过滤器的核心
      3. 认证成功,则执行successfulAuthentication(),将已认证的Authentication存放到SecurityContext,认证失败则通过认证失败处理器AuthenticationFailureHandler 处理
      4. 接下来研究下attemptAuthentication方法,这个方法在当前父类中是一个抽象方法,由子类实现,而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. 然后从当前请求中获取用户名username和密码password

      3. 通过当前用户名和密码,构造一个令牌UsernamePasswordAuthenticationToken

        这个UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken,而AbstractAuthenticationToken又实现了Authentication接口,所以实质上这个token就是一个Authentication对象

      4. 最后调用this.getAuthenticationManager().authenticate(authRequest)返回,这里就用到了AuthenticationManager去认证了,这个稍后在看

    5. 接着看这个方法this.getAuthenticationManager().authenticate(authRequest)

      这里使用的是AuthenticationManager接口的方法去进行认证,这个方法authenticate很奇特,传入的参数和返回值类型都是Authentication.

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

      该接口方法的作用是:**对用户未认证的凭据进行认证,认证通过后返回已认证的凭据,否则抛出认证异常AuthenticationException

      分析:

      ​ 从源码可以看到,这个AuthenticationManager是一个接口,所以他并不是真正做事情的那个,只是提供了一个标准,真正实现功能的是它的子类。

      通过(ctrl + h)查看AuthenticationManager接口的实现类,可以看到如下:

      AuthenticationManager-impl.jpg

      其他几个都是内部类,所以我们找到了ProviderManager实现了AuthenticationManager

      我们看看他实现的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依次进行认证
          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都认证失败,且之前有构造一个 AuthenticationManager 实现类,那么利用AuthenticationManager 实现类 继续认证
          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;
              }
          }
          // 认证成功
          if (result != null) {
              if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
                  // Authentication is complete. Remove credentials and other secret data
                  // from authentication
                  // 认证成功后删除验证信息
                  ((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
              // 发布登录成功事件
              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并不是自己直接对请求进行验证,而是循环一个AuthenticationProvider列表,列表中每一个provider依次进行判断是否使用它进行验证。

    6. 接下来看看AuthenticationProvider

      这个AuthenticationProvider也是一个接口

      public interface AuthenticationProvider {
          // 认证方法
      	Authentication authenticate(Authentication authentication) throws AuthenticationException;
      	// 该Provider是否支持对应的Authentication类型
      	boolean supports(Class<?> authentication);
      }
      

      同样看看这个接口有哪些实现类:

      AuthenticationProvider.jpg

      这个接口的实现类和继承类有很多,我们直接看与User相关的,会看到有一个AbstractUserDetailsAuthenticationProvider抽象类,他的实现类是DaoAuthenticationProvider, 才是真正做验证的人

      authenticate是由AbstractUserDetailsAuthenticationProvider`实现的,源码如下:

      @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是一个抽象方法,子类中实现
                  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是一个抽象方法,子类中实现
              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();
          }
          // 创建一个成功的Authentication对象返回
          return createSuccessAuthentication(principalToReturn, authentication, user);
      }
      

      在这个authenticate方法里,真正做验证的方法是:retrieveUser,该方法是在子类DaoAuthenticationProvider中实现的

      @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);
          }
      }
      // 重写了父类的方法,对密码进行一些加密操作
      @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方法,就是调用UserDetailsServiceloadUserByUsername方法,这个UserDetailsService就是一个服务接口,加载UserDetails,一般是从数据库中去查找用户,封装为UserDetails对象返回,找不到就报异常。

      ​ SpringSecurity默认实现了一个UserDetails的实现类User,当我们使用将用户信息存储在内存的方式auth.inMemoryAuthentication()时,会创建一个InMemoryUserDetailsManager,这个类创建了一个UserDetails的实现类User,

      同时这个类实现了UserDetailsManager接口,而UserDEtailsManager又是继承自UserDetailsService,所以默认情况下的话就是调用InMemoryUserDetailsManager类的loadUserByUsername方法

      ​ 因此,当我们需要自定义时,则需要自己实现UserDetailsService接口和UserDetails接口

    7. UserDetailsServiceUserDetails接口

      UserDetailsService就是定义了一个加载UserDetails的接口,通常我们会实现这个接口,然后从数据库中查询相关用户信息,再返回。

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

      UserDetails也是一个接口,在实际开发中也会对他进行定制化,提供核心用户信息。

      SpringSecurity处于安全考虑,UserDetails只是存储用户信息,这些信息最后会封装到Authentication对象中的。

      public interface UserDetails extends Serializable {
      	// 返回用户的权限集合
      	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. 至此,整个认证流程差不多就走完了,这个过程中,梳理以下,我们是以默认的登录方式来分析流程的,默认的登录方式用到的是:UsernamePasswordAuthenticationFilterUsernamePasswordAuthenticationToken以及DaoAuthenticationProvider这些来进行身份的验证,那么以后我们要添加别的验证方式的话,就可以模仿这个流程:重新继承AbstractAuthenticationProcessingFilter,AbstractAuthenticationToken,AuthenticationProvider

      流程图大致如下:

      AuthenticationProvider-1.jpg

  2. 返回过程

    1. DaoAuthenticationProvider类的retrieveUser方法通过loadUserByUsername获取到用户信息后返回一个UserDetails对象给到父类AbstractUserDetailsAuthenticationProvider的方法authenticate
    2. AbstractUserDetailsAuthenticationProvider拿到返回的UserDetails后,调用了return createSuccessAuthentication(principalToReturn, authentication, user);创建了一个可信的UsernamepasswordAuthenticationToken,并返回给了ProviderManagerauthenticate方法
    3. 这时候的UsernamepasswordAuthenticationToken是已验证过的可信的,再往上返回AuthenticationUsernamepasswordAuthenticationToken是他的一个实现类,多态)
    4. 再回到了UsernamepasswordAuthenticationFilter类的attemptAuthentication方法中,return this.getAuthenticationManager().authenticate(authRequest)返回到了AbstractAuthenticationProcessingFilter类中doFilter,最后调用了successfulAuthentication,将可信的Authentication对象保存到SecurityContext中,然后放行。

5. 整体流程图

flow.jpg

6. 学习博客

【项目实践】一文带你搞定前后端分离下的认证和授权|Spring Security + JWT

SpringSecurity+JWT认证流程解析 | 掘金新人第一弹

SpringSecurit(小胖哥)

标签:username,SpringSecurity,流程,request,authentication,认证,Authentication,null
来源: https://www.cnblogs.com/LucasBlog/p/16026370.html