1. 前言
SpringSecurity的认证,其实就是我们的登录验证。
Web系统中登录验证的核心就是**凭证**,比较多使用的是`Session`和`JWT`,其原理都是在用户成功登录后返回给用户一个凭证,后续用户访问时需要携带凭证来辨别自己的身份。后端会根据这个凭证进行安全判断,如果凭证没问题则代表已登录,否则则直接拒绝请求。
以下内容会先分析源码理清楚认证的流程。
2. SpringSecurity的工作流程
要想分析SpringSecurity的认证流程,就一定要先了解整个SpringSecurity的工作流程,我们才能最终进行一些自定义操作。
Spring Security的web基础是Filters(过滤器),它是通过一层层的Filters来对web请求做处理的,每一个web请求会经过一条过滤器链,在这个过程中完成认证与授权。
其具体工作流程是这样的:
-
在Spring的过滤器链中,Spring Security向其添加了一个
FilterChainProxy
过滤器,而这个过滤器只是一个代理过滤器,通过这个代理过滤器创建一套SpringSecurity自定义的过滤器链(认证与授权过滤器就在这过滤器链中),然后再执行这一系列自定义的过滤器。如图所示(网上找的) -
然后我们可以来看看这个代理过滤器
FilterChainProxy
的部分源码:下面debug程序,设置断点,看看过滤器链中有哪些过滤器
这些过滤器中,我们重点关注
UsernamePasswordAuthenticationFilter
和FilterSecurityInterceptor
,其中UsernamePasswordAuthenticationFilter
负责登录认证,FilterSecurityInterceptor
负责授权。 -
SpringSecurity的基本原理(网络上找的一张图)
如图所示,一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权,对应了我们代码debug的过滤器链。
注意:只有在配置中打开了
formLogin
配置项,过滤器链中才会加入它们,否则是不会被加到过滤器链的。SpringSecurity中有两个配置项叫
formLogin
和httpBasic
,分别对应着表单认证方式(过滤器是UsernamePasswordAuthenticationFilter)和Basic认证方式(过滤器是BasicAuthenticationFilter),分别对应上图
3. SpringSecurity中的重要组件
Authentication
:认证接口,存储了认证信息,代表当前登录用户SecurityContext
:上下文对象,Authentication
对象会放在里面SecurityContextHolder
:用于拿到上下文对象的静态工具类AuthenticationManager
:用于校验Authentication
,返回一个认证完成后的Authentication
对象
我们需要通过
SecurityContext
上下文对象来获取认证对象Authentication
,而SecurityContext
又是交给SecurityContextHolder
进行管理的。-
查看源码
-
SecurityContext:
接口只有两个方法,作用就是get/set Authentication
-
SecurityContextHolder:
可以人为这是
SecurityContext
的一个静态工具类,主要有get,set,clear处理SecurityContext
,其原理是使用ThreadLocal
来保证一个线程中传递同一个对象
-
-
我们可以通过以下代码在程序任何地方获取
Authentication
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-
再看看
Authentication
和AuthenticationManager
源码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:
该接口定义了一个认证方法,将一个未被认证的
Authentication
传入,返回一个已认证的Authentication
-
-
总结下SpringSecurity的认证流程
将上面四个组件串联起来,可以大致了解到认证的流程:
- 一个请求带着身份信息进来
- 经过
AuthenticationManager
进行认证 - 然后通过
SecurityContextHolder
获取到SecurityContext
- 最后将认证后的
Authentication
放入SecurityContext
,这样下一个请求进来就能知道是否已认证过
4. 完整源码流程
有了以上的一些基础了解后,我们来顺着源码流程走一边,理清整个认证的流程。
基于formLogin的流程分析,SpringSecurity默认也是formLogin。
以下源码我都将注释去掉,否则太长了!
-
第一步:请求进来,到达
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; } }
分析:
过滤器中定义了一些默认的信息,比如默认用户名参数为username,密码参数为password,默认请求为
/login
,但同时也提供了set、get方法让我们自定义,自定义的方式就是在配置类WebSecurityConfigurerAdapter
的子类中重写configure(HttpSecurity http)
设置过滤器的处理核心就是
doFilter
,但我们在UsernamePasswordAuthenticationFilter
中并没有看到,这是因为在他父类AbstractAuthenticationProcessingFilter
实现了。-
进入
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); }
分析:
- doFilter首先判断uri是否需要认证
- 接着执行方法
Authentication authenticationResult = attemptAuthentication(request, response);
进行认证,从函数名也能看出是尝试认证,认证成功获取认证对象Authentication
,这是这个过滤器的核心 - 认证成功,则执行
successfulAuthentication()
,将已认证的Authentication
存放到SecurityContext
,认证失败则通过认证失败处理器AuthenticationFailureHandler
处理 - 接下来研究下
attemptAuthentication
方法,这个方法在当前父类中是一个抽象方法,由子类实现,而AbstractAuthenticationProcessingFilter
的一个子类就是UsernamepasswordAuthenticationFilter
,回到这个类看看这个attemptAuthentication
方法
-
分析
UsernamepasswordAuthenticationFilter
的attemptAuthentication
方法@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); }
分析:
从源码可以看出,首先如果不是POST请求,直接抛出异常
然后从当前请求中获取用户名username和密码password
-
通过当前用户名和密码,构造一个令牌
UsernamePasswordAuthenticationToken
这个
UsernamePasswordAuthenticationToken
继承了AbstractAuthenticationToken
,而AbstractAuthenticationToken
又实现了Authentication
接口,所以实质上这个token就是一个Authentication
对象 最后调用
this.getAuthenticationManager().authenticate(authRequest)
返回,这里就用到了AuthenticationManager
去认证了,这个稍后在看
-
接着看这个方法
this.getAuthenticationManager().authenticate(authRequest)
这里使用的是
AuthenticationManager
接口的方法去进行认证,这个方法authenticate
很奇特,传入的参数和返回值类型都是Authentication
.public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
该接口方法的作用是:**对用户未认证的凭据进行认证,认证通过后返回已认证的凭据,否则抛出认证异常
AuthenticationException
分析:
从源码可以看到,这个
AuthenticationManager
是一个接口,所以他并不是真正做事情的那个,只是提供了一个标准,真正实现功能的是它的子类。通过(ctrl + h)查看
AuthenticationManager
接口的实现类,可以看到如下:其他几个都是内部类,所以我们找到了
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依次进行判断是否使用它进行验证。 -
接下来看看
AuthenticationProvider
这个
AuthenticationProvider
也是一个接口public interface AuthenticationProvider { // 认证方法 Authentication authenticate(Authentication authentication) throws AuthenticationException; // 该Provider是否支持对应的Authentication类型 boolean supports(Class> authentication); }
同样看看这个接口有哪些实现类:
这个接口的实现类和继承类有很多,我们直接看与
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
方法,就是调用UserDetailsService
的loadUserByUsername
方法,这个UserDetailsService
就是一个服务接口,加载UserDetails
,一般是从数据库中去查找用户,封装为UserDetails对象返回,找不到就报异常。SpringSecurity默认实现了一个UserDetails的实现类User,当我们使用将用户信息存储在内存的方式
auth.inMemoryAuthentication()
时,会创建一个InMemoryUserDetailsManager
,这个类创建了一个UserDetails
的实现类User
,同时这个类实现了
UserDetailsManager
接口,而UserDEtailsManager
又是继承自UserDetailsService
,所以默认情况下的话就是调用InMemoryUserDetailsManager
类的loadUserByUsername
方法因此,当我们需要自定义时,则需要自己实现
UserDetailsService
接口和UserDetails
接口 -
UserDetailsService
和UserDetails
接口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 *
null
. * @return the username (nevernull
) */ String getUsername(); // 用户账户是否过期 boolean isAccountNonExpired(); // 用户是否被锁定 boolean isAccountNonLocked(); // 用户的密码是否已过期 boolean isCredentialsNonExpired(); // 用户是否被禁用 boolean isEnabled(); } -
至此,整个认证流程差不多就走完了,这个过程中,梳理以下,我们是以默认的登录方式来分析流程的,默认的登录方式用到的是:
UsernamePasswordAuthenticationFilter
和UsernamePasswordAuthenticationToken
以及DaoAuthenticationProvider
这些来进行身份的验证,那么以后我们要添加别的验证方式的话,就可以模仿这个流程:重新继承AbstractAuthenticationProcessingFilter
,AbstractAuthenticationToken
,AuthenticationProvider
。流程图大致如下:
-
返回过程
DaoAuthenticationProvider
类的retrieveUser
方法通过loadUserByUsername
获取到用户信息后返回一个UserDetails
对象给到父类AbstractUserDetailsAuthenticationProvider
的方法authenticate
中AbstractUserDetailsAuthenticationProvider
拿到返回的UserDetails
后,调用了return createSuccessAuthentication(principalToReturn, authentication, user);
创建了一个可信的UsernamepasswordAuthenticationToken
,并返回给了ProviderManager
的authenticate
方法这时候的
UsernamepasswordAuthenticationToken
是已验证过的可信的,再往上返回Authentication
(UsernamepasswordAuthenticationToken
是他的一个实现类,多态)再回到了
UsernamepasswordAuthenticationFilter
类的attemptAuthentication
方法中,return this.getAuthenticationManager().authenticate(authRequest)
返回到了AbstractAuthenticationProcessingFilter
类中doFilter
,最后调用了successfulAuthentication
,将可信的Authentication
对象保存到SecurityContext
中,然后放行。-
认证成功Handler与失败Handler
UsernamepasswordAuthenticationFilter
的父类AbstractAuthenticationProcessingFilter
类的doFilter
方法中,认证成功会调用successfulAuthentication
,失败调用unsuccessfulAuthentication
。在
AbstractAuthenticationProcessingFilter
中对successfulAuthentication
方法和unsuccessfulAuthentication
做了默认实现,源码如下: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); }
分析:
- 方法最后分别对认证成功和失败做了自定义处理,最后分别调用了两个Handler处理,
AuthenticationSuccessHandler
类型的successHandler
和AuthenticationFailureHandler
类型的failureHandler
。 - 在
AbstractAuthenticationProcessingFilter
类中还分别提供了setAuthenticationSuccessHandler
方法和setAuthenticationFailureHandler
能让我们实现自定义Handler的注入。 - 综上分析,我们要自定义Handler就有两种方式了
- 自定义
AuthenticationSuccessHandler
类和AuthenticationFailureHandler
分别实现onAuthenticationSuccess
方法和onAuthenticationFailure
方法,然后调用提供的set方法注入到类中 -
AbstractAuthenticationProcessingFilter
的子类中去重写successfulAuthentication
方法和unsuccessfulAuthentication
方法。
- 自定义
- 方法最后分别对认证成功和失败做了自定义处理,最后分别调用了两个Handler处理,
5. 整体流程图
6. 学习博客
【项目实践】一文带你搞定前后端分离下的认证和授权|Spring Security + JWT
SpringSecurity+JWT认证流程解析 | 掘金新人第一弹
SpringSecurit(小胖哥)