一、前言
在本系列文章中介绍了过滤器和相关认证组件,对部分源码也进行详细分析。
本章主要学习 Spring Security
中通过 HTML
表单提供用户名和密码的认证流程。
二、配置表单登录
默认情况下,
Spring Security
表单登录处于启用状态。 但是,一旦提供了任何基于servlet
的配置,就必须显式提供基于表单的登录。
一个最小的显式 Java
配置示例:
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {/*** 构建SecurityFilterChain** @param http* @return* @throws Exception*/@BeanSecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {// 配置所有http请求必须经过认证http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated());// 开启表单认证(默认配置)http.formLogin(Customizer.withDefaults());// 构造器构建SecurityFilterChain对象return http.build();}/*** 配置登录名密码** @return*/@Beanpublic UserDetailsService userDetailsService() {UserDetails user = User.withUsername("admin").password("{noop}123456").roles("USER").build();return new InMemoryUserDetailsManager(new UserDetails[]{user});}
}
配置中 http.formLogin(Customizer.withDefaults())
表示开启表单认证,该方法中( SecurityFilterChain
的构造器 HttpSecurity
)应用了一个表单登录配置器 FormLoginConfigurer
:
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));return HttpSecurity.this;
}
在本系列章节中对 FormLoginConfigurer
进行过介绍,会向 SecurityFilterChain
添加 UsernamePasswordAuthenticationFilter
用于表单认证,并设置用户名和密码对应的请求参数名称:
public FormLoginConfigurer() {super(new UsernamePasswordAuthenticationFilter(), null);usernameParameter("username");passwordParameter("password");
}
更多 FormLoginConfigurer
介绍请见:Spring Security 6.x 系列(10)—— SecurityConfigurer 配置器及其分支实现源码分析(二)
添加一个访问测试接口:
@GetMapping("/private")
public String hello() {return "hello spring security";
}
三、认证流程分析
3.1 重定向登录页
上文配置所有 http
请求必须经过认证,在未登录时访问 /private
接口,会重定向登录页,流程如下:
① 首先,用户向未授权的资源 /private
发出未经身份认证的请求。
② Spring Security
的 AuthorizationFilter
抛出 AccessDeniedException
异常。
③ 由于用户未经过身份验证,因此 ExceptionTranslationFilter
将启动“启动身份验证”,并使用配置的 AuthenticationEntryPoint
将重定向发送到登录页。 在大多数情况下,是 LoginUrlAuthenticationEntryPoint
的实例。
④ 浏览器请求重定向到的登录页面。
⑤ 呈现默认登录页面。
下面我们会针对上述流程进行详细的源码分析。
3.1.1 抛出 AccessDeniedException 异常
/private
发出未经身份认证的请求会依次通过下述所有过滤器:
Security filter chain: [DisableEncodeUrlFilterWebAsyncManagerIntegrationFilterSecurityContextHolderFilterHeaderWriterFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterDefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilterRequestCacheAwareFilterSecurityContextHolderAwareRequestFilterAnonymousAuthenticationFilterExceptionTranslationFilterAuthorizationFilter
]
在进入最后一个过滤器 AuthorizationFilter
时,会对当前请求做最后的权限效验,如何未被授权,会抛出 AccessDeniedException
异常:
首先 AuthorizationFilter
会取出当前用户认证信息,因当前请求未经身份认证,获取的 Authentication
为 AnonymousAuthenticationFilter
创建的匿名用户:
接着使用 authorizationManager
授权管理器对当前认证信息进行检查,因为是匿名用户,所有判定当前请求无权访问,抛出 AccessDeniedException
异常:
3.1.2 异常处理
抛出 AccessDeniedException
异常会被 ExceptionTranslationFilter
捕获:
ExceptionTranslationFilter
根据异常类型进行相应处理:
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,FilterChain chain, RuntimeException exception) throws IOException, ServletException {if (exception instanceof AuthenticationException) {handleAuthenticationException(request, response, chain, (AuthenticationException) exception);}else if (exception instanceof AccessDeniedException) {handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);}
}
分别为:
- 调用
handleAuthenticationException
方法处理AuthenticationException
异常类型 - 调用
handleAccessDeniedException
方法处理AccessDeniedException
异常类型
因本次异常类型为 AccessDeniedException
,故调用 handleAccessDeniedException
方法:
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,FilterChain chain, AuthenticationException exception) throws ServletException, IOException {this.logger.trace("Sending to authentication entry point since authentication failed", exception);sendStartAuthentication(request, response, chain, exception);
}private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {if (logger.isTraceEnabled()) {logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",authentication), exception);}sendStartAuthentication(request, response, chain,new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication","Full authentication is required to access this resource")));}else {if (logger.isTraceEnabled()) {logger.trace(LogMessage.format("Sending %s to access denied handler since access is denied", authentication),exception);}this.accessDeniedHandler.handle(request, response, exception);}
}
因为是匿名用户需接着调用 sendStartAuthentication
方法缓存请求(用于认证成功后再重定向回此请求),并调用AuthenticationEntryPoint
生成认证入口:
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,AuthenticationException reason) throws ServletException, IOException {// SEC-112: Clear the SecurityContextHolder's Authentication, as the// existing Authentication is no longer considered validSecurityContext context = this.securityContextHolderStrategy.createEmptyContext();this.securityContextHolderStrategy.setContext(context);this.requestCache.saveRequest(request, response);this.authenticationEntryPoint.commence(request, response, reason);
}
3.1.3 重定向
AuthenticationEntryPoint
将重定向发送到登录页。 在大多数情况下,是 LoginUrlAuthenticationEntryPoint
的实例:
调用 commence
方法进行重定向或转发:
上述流程运行日志:
2023-12-18T14:55:16.292+08:00 INFO 24072 --- [nio-9000-exec-3] Spring Security Debugger : ************************************************************Request received for GET '/priavte':org.apache.catalina.connector.RequestFacade@1299acf6servletPath:/priavte
pathInfo:null
headers:
host: 127.0.0.1:9000
connection: keep-alive
sec-ch-ua: "Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
sec-fetch-site: none
sec-fetch-mode: navigate
sec-fetch-user: ?1
sec-fetch-dest: document
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cookie: mttsmart-access-token=1ba0d263-c764-4916-8dfd-79fad8a9198f; mttsmart-refresh-token=dqcIyoWLvZzYbNvAGGRKykCkF1oB6k0YvrRsg-bT4XFoI-5dEKmbdYTST5op_s8UxzKaR7ON6N8H_N4kqAHtOtd9GdRtEznZqQ0F96bxIbvpOkxM-ReyVaDgRqTdCsHi; token=034a4acc-fd5f-46b3-804f-dab458b9157e; refresh_token=C-NYlk79Hkxc1oW8iZ9aFF-okp0ZDmODCwIQtcGcBFS3FLWViNLPW50ccVQzquYo1rYqK1TUqmMvk9ZnqsLIOxjI4Mff-UxuMwJKExN0uuXP_wRxrrGytXqvSuqkIvDI; JSESSIONID=6A5FA61657FA3C84115F42CBF6CE370BSecurity filter chain: [DisableEncodeUrlFilterWebAsyncManagerIntegrationFilterSecurityContextHolderFilterHeaderWriterFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterDefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilterRequestCacheAwareFilterSecurityContextHolderAwareRequestFilterAnonymousAuthenticationFilterExceptionTranslationFilterAuthorizationFilter
]************************************************************2023-12-18T14:55:16.292+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@40729f01, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@d535a3d, org.springframework.security.web.context.SecurityContextHolderFilter@7c2924d7, org.springframework.security.web.header.HeaderWriterFilter@9efcd90, org.springframework.security.web.csrf.CsrfFilter@3a1706e1, org.springframework.security.web.authentication.logout.LogoutFilter@4ecd00b5, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@506aabf6, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@3eb3232b, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2d760326, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@6587305a, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@74d6736, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@9e54c59, org.springframework.security.web.access.ExceptionTranslationFilter@365cdacf, org.springframework.security.web.access.intercept.AuthorizationFilter@3330f3ad]] (1/1)
2023-12-18T14:55:16.292+08:00 DEBUG 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Securing GET /priavte
2023-12-18T14:55:16.292+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/14)
2023-12-18T14:55:16.292+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.s.w.a.logout.LogoutFilter : Did not match request to Ant [pattern='/logout', POST]
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking UsernamePasswordAuthenticationFilter (7/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] w.a.UsernamePasswordAuthenticationFilter : Did not match request to Ant [pattern='/login', POST]
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking DefaultLoginPageGeneratingFilter (8/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking DefaultLogoutPageGeneratingFilter (9/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] .w.a.u.DefaultLogoutPageGeneratingFilter : Did not render default logout page since request did not match [Ant [pattern='/logout', GET]]
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (10/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.s.w.s.HttpSessionRequestCache : matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (11/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking AnonymousAuthenticationFilter (12/14)
2023-12-18T14:55:16.293+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (13/14)
2023-12-18T14:55:16.294+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (14/14)
2023-12-18T14:55:16.294+08:00 TRACE 24072 --- [nio-9000-exec-3] estMatcherDelegatingAuthorizationManager : Authorizing SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@1c5e50d5]
2023-12-18T14:55:16.294+08:00 TRACE 24072 --- [nio-9000-exec-3] estMatcherDelegatingAuthorizationManager : Checking authorization on SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@1c5e50d5] using org.springframework.security.authorization.AuthenticatedAuthorizationManager@3d9544f9
2023-12-18T14:55:23.778+08:00 TRACE 24072 --- [nio-9000-exec-3] w.c.HttpSessionSecurityContextRepository : Did not find SecurityContext in HttpSession 6A5FA61657FA3C84115F42CBF6CE370B using the SPRING_SECURITY_CONTEXT session attribute
2023-12-18T14:55:23.778+08:00 TRACE 24072 --- [nio-9000-exec-3] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2023-12-18T14:55:23.778+08:00 TRACE 24072 --- [nio-9000-exec-3] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2023-12-18T14:55:23.778+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=6A5FA61657FA3C84115F42CBF6CE370B], Granted Authorities=[ROLE_ANONYMOUS]]
2023-12-18T15:18:13.558+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.s.w.a.ExceptionTranslationFilter : Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=6A5FA61657FA3C84115F42CBF6CE370B], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is deniedorg.springframework.security.access.AccessDeniedException: Access Deniedat org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:98) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter.doFilterInternal(DefaultLogoutPageGeneratingFilter.java:58) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.doFilter(DefaultLoginPageGeneratingFilter.java:188) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.doFilter(DefaultLoginPageGeneratingFilter.java:174) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.debug.DebugFilter.invokeWithWrappedRequest(DebugFilter.java:90) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:78) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:67) ~[spring-security-web-6.1.4.jar:6.1.4]at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268) ~[spring-web-6.0.12.jar:6.0.12]at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.12.jar:6.0.12]at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.12.jar:6.0.12]at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.12.jar:6.0.12]at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.13.jar:10.1.13]at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]2023-12-18T15:18:13.561+08:00 DEBUG 24072 --- [nio-9000-exec-3] o.s.s.w.s.HttpSessionRequestCache : Saved request http://127.0.0.1:9000/priavte?continue to session
2023-12-18T15:18:13.561+08:00 DEBUG 24072 --- [nio-9000-exec-3] o.s.s.web.DefaultRedirectStrategy : Redirecting to http://127.0.0.1:9000/login
2023-12-18T15:18:13.562+08:00 TRACE 24072 --- [nio-9000-exec-3] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
3.1.4 生成默认页面
重定向后浏览器地址变为:http://127.0.0.1:9000/login
,发起 Get
请求,此时又开始执行过滤器:
2023-12-18T15:18:13.566+08:00 INFO 24072 --- [nio-9000-exec-5] Spring Security Debugger : ************************************************************Request received for GET '/login':org.apache.catalina.connector.RequestFacade@13754d1bservletPath:/login
pathInfo:null
headers:
host: 127.0.0.1:9000
connection: keep-alive
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
sec-fetch-site: none
sec-fetch-mode: navigate
sec-fetch-user: ?1
sec-fetch-dest: document
sec-ch-ua: "Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cookie: mttsmart-access-token=1ba0d263-c764-4916-8dfd-79fad8a9198f; mttsmart-refresh-token=dqcIyoWLvZzYbNvAGGRKykCkF1oB6k0YvrRsg-bT4XFoI-5dEKmbdYTST5op_s8UxzKaR7ON6N8H_N4kqAHtOtd9GdRtEznZqQ0F96bxIbvpOkxM-ReyVaDgRqTdCsHi; token=034a4acc-fd5f-46b3-804f-dab458b9157e; refresh_token=C-NYlk79Hkxc1oW8iZ9aFF-okp0ZDmODCwIQtcGcBFS3FLWViNLPW50ccVQzquYo1rYqK1TUqmMvk9ZnqsLIOxjI4Mff-UxuMwJKExN0uuXP_wRxrrGytXqvSuqkIvDI; JSESSIONID=6A5FA61657FA3C84115F42CBF6CE370BSecurity filter chain: [DisableEncodeUrlFilterWebAsyncManagerIntegrationFilterSecurityContextHolderFilterHeaderWriterFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterDefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilterRequestCacheAwareFilterSecurityContextHolderAwareRequestFilterAnonymousAuthenticationFilterExceptionTranslationFilterAuthorizationFilter
]************************************************************2023-12-18T15:18:13.566+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@40729f01, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@d535a3d, org.springframework.security.web.context.SecurityContextHolderFilter@7c2924d7, org.springframework.security.web.header.HeaderWriterFilter@9efcd90, org.springframework.security.web.csrf.CsrfFilter@3a1706e1, org.springframework.security.web.authentication.logout.LogoutFilter@4ecd00b5, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@506aabf6, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@3eb3232b, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2d760326, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@6587305a, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@74d6736, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@9e54c59, org.springframework.security.web.access.ExceptionTranslationFilter@365cdacf, org.springframework.security.web.access.intercept.AuthorizationFilter@3330f3ad]] (1/1)
2023-12-18T15:18:13.567+08:00 DEBUG 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Securing GET /login
2023-12-18T15:18:13.567+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/14)
2023-12-18T15:18:13.567+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/14)
2023-12-18T15:18:13.567+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/14)
2023-12-18T15:18:13.567+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/14)
2023-12-18T15:18:13.568+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/14)
2023-12-18T15:18:13.568+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
2023-12-18T15:18:13.568+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/14)
2023-12-18T15:18:13.568+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.s.w.a.logout.LogoutFilter : Did not match request to Ant [pattern='/logout', POST]
2023-12-18T15:18:13.568+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Invoking UsernamePasswordAuthenticationFilter (7/14)
2023-12-18T15:18:13.568+08:00 TRACE 24072 --- [nio-9000-exec-5] w.a.UsernamePasswordAuthenticationFilter : Did not match request to Ant [pattern='/login', POST]
2023-12-18T15:18:13.568+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : Invoking DefaultLoginPageGeneratingFilter (8/14)
2023-12-18T15:18:13.568+08:00 TRACE 24072 --- [nio-9000-exec-5] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
在经过 DefaultLoginPageGeneratingFilter
时,进行默认登录页处理,后续过滤器操作不再执行:
调用 response
直接输出一个页面,并 return
不再执行后续操作,最后登录页面效果:
3.2 表单登录
提交用户名和密码后,将对用户名和密码进行身份验:
①当用户输入用户名和密码提交登录,登录请求会被 UsernamePasswordAuthenticationFilter
处理,预构建认证对象 UsernamePasswordAuthenticationToken
(未认证) 。
②接下来调用 AuthenticationManager
进行认证。
ProviderManager
是AuthenticationManager
的实现类,ProviderManager
遍历所有的认证提供者,DaoAuthenticationProvider
符合Form
表单认证。
③如果身份认证失败:
-
清理
SecurityContextHolder
。 -
RememberMeServices#loginFail
被调用。 如果未配置“记住我”,则为空操作。 -
AuthenticationFailureHandler
被调用,重定向/login?error
登录页面,显示错误信息 。
④如果身份验证成功:
-
SessionAuthenticationStrategy
会话处理。 -
在
SecurityContextHolder
上存储认证信息。 -
RememberMeServices#loginSuccess
被调用。 如果未配置“记住我”,则为空操作。 -
ApplicationEventPublisher#InteractiveAuthenticationSuccessEvent
被调用发布认证成功事件。 -
AuthenticationSuccessHandler
被调用,重定向登录前URL
。
3.2.1 进入 AbstractAuthenticationProcessingFilter
表单登录时,登录请求会进入到 UsernamePasswordAuthenticationFilter
,该过滤器会拦截浏览器提交的表单登录请求并进行身份认证。
UsernamePasswordAuthenticationFilter
继承自父类 AbstractAuthenticationProcessingFilter
,UsernamePasswordAuthenticationFilter
没有对父类的 AbstractAuthenticationProcessingFilter
的 doFilter
方法进行重写,故实际执行的是父类 AbstractAuthenticationProcessingFilter
的 doFilter
方法。
采用模版模式,根据不同的认证方式,执行不同子类的认证逻辑。
AbstractAuthenticationProcessingFilter
的 doFilter
方法几乎完成认证的所有流程:
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 {Authentication authenticationResult = attemptAuthentication(request, response);if (authenticationResult == null) {// return immediately as subclass has indicated that it hasn't completedreturn;}this.sessionStrategy.onAuthentication(authenticationResult, request, response);// Authentication successif (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 failedunsuccessfulAuthentication(request, response, ex);}
}/*** Performs actual authentication.* <p>* The implementation should do one of the following:* <ol>* <li>Return a populated authentication token for the authenticated user, indicating* successful authentication</li>* <li>Return null, indicating that the authentication process is still in progress.* Before returning, the implementation should perform any additional work required to* complete the process.</li>* <li>Throw an <tt>AuthenticationException</tt> if the authentication process* fails</li>* </ol>* @param request from which to extract parameters and perform the authentication* @param response the response, which may be needed if the implementation has to do a* redirect as part of a multi-stage authentication process (such as OIDC).* @return the authenticated user token, or null if authentication is incomplete.* @throws AuthenticationException if authentication fails.*/
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException, IOException, ServletException;/*** Default behaviour for successful authentication.* <ol>* <li>Sets the successful <tt>Authentication</tt> object on the* {@link SecurityContextHolder}</li>* <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>* <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured* <tt>ApplicationEventPublisher</tt></li>* <li>Delegates additional behaviour to the* {@link AuthenticationSuccessHandler}.</li>* </ol>** Subclasses can override this method to continue the {@link FilterChain} after* successful authentication.* @param request* @param response* @param chain* @param authResult the object returned from the <tt>attemptAuthentication</tt>* method.* @throws IOException* @throws ServletException*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authResult) throws IOException, ServletException {SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();context.setAuthentication(authResult);this.securityContextHolderStrategy.setContext(context);this.securityContextRepository.saveContext(context, request, response);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);
}/*** Default behaviour for unsuccessful authentication.* <ol>* <li>Clears the {@link SecurityContextHolder}</li>* <li>Stores the exception in the session (if it exists or* <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li>* <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li>* <li>Delegates additional behaviour to the* {@link AuthenticationFailureHandler}.</li>* </ol>*/
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,AuthenticationException failed) throws IOException, ServletException {this.securityContextHolderStrategy.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);
}
3.2.2 进入 UsernamePasswordAuthenticationFilter
随后进入 UsernamePasswordAuthenticationFilter
的 attemptAuthentication
方法,该方法会预构建认证对象 UsernamePasswordAuthenticationToken
(未认证):
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.trim() : "";String password = obtainPassword(request);password = (password != null) ? password : "";UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,password);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);
}
UsernamePasswordAuthenticationToken
刚创建时,包含输入的用户名、密码、客户端IP
、sessionID
等信息,这时是未认证状态:
ProviderManager
是 AuthenticationManager
的实现类,ProviderManager
遍历所有的认证提供者,DaoAuthenticationProvider
符合 Form
表单认证,调用其 authenticate
方法进行认证。
3.2.3 进入 DaoAuthenticationProvider
UsernamePasswordAuthenticationToken
类型的 Authentication
对象是由 DaoAuthenticationProvider
进行认证处理 ,和上文的 AbstractAuthenticationProcessingFilter
类似,也是采用模版模式,首先调用的是父类 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);
}
其中有两个比较重要的点:
- 调用子类
DaoAuthenticationProvider
的retrieveUser
方法,根据用户名获取用户信息 - 调用子类
DaoAuthenticationProvider
的additionalAuthenticationChecks
方法,校验密码
在校验密码成功后,AbstractUserDetailsAuthenticationProvider
会创建一个认证成功的 Authentication
对象:
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,UserDetails user) {// Ensure we return the original credentials the user supplied,// so subsequent attempts are successful even with encoded passwords.// Also ensure we return the original getDetails(), so that future// authentication events after cache expiry contain the detailsUsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));result.setDetails(authentication.getDetails());this.logger.debug("Authenticated user");return result;
}
3.2.4 认证成功处理
3.2.4.1 会话策略处理
回到 3.2.1 中的 doFilter
方法进行认证成功的后续处理:
CsrfAuthenticationStrategy
:它负责在执行认证请求之后,删除旧的令牌生成新的,确保每次请求之后,csrf-token
都得到更新。ChangeSessionIdAuthenticationStrategy
:主要是使用HttpServletRequest.changeSessionId()
方法修改sessionID
来防止会话固定攻击。
3.2.4.2 调用 successfulAuthentication
会话处理完成后,调用 successfulAuthentication
进行认证成功后续处理:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authResult) throws IOException, ServletException {SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();context.setAuthentication(authResult);this.securityContextHolderStrategy.setContext(context);this.securityContextRepository.saveContext(context, request, response);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);
}
执行以下业务逻辑:
-
在
SecurityContextHolder
上存储认证信息。 -
RememberMeServices#loginSuccess
被调用。 如果未配置“记住我”,则为空操作。 -
ApplicationEventPublisher#InteractiveAuthenticationSuccessEvent
被调用发布认证成功事件。 -
AuthenticationSuccessHandler
被调用,重定向登录前URL
。此时
AuthenticationSuccessHandler
的实现为SavedRequestAwareAuthenticationSuccessHandler
。
3.2.5 认证失败处理
如果认证失败,比如密码错误,在 AbstractUserDetailsAuthenticationProvider
的 authenticate
方法中抛出以下异常信息:
org.springframework.security.authentication.BadCredentialsException: 用户名或密码错误
之后会进入到 AbstractAuthenticationProcessingFilter
失败处理逻辑中:
失败处理方法如下:
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,AuthenticationException failed) throws IOException, ServletException {this.securityContextHolderStrategy.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);
}
执行以下业务逻辑:
-
清理
SecurityContextHolder
。 -
RememberMeServices#loginFail
被调用。 如果未配置“记住我”,则为空操作。 -
AuthenticationFailureHandler
被调用,重定向/login?error
登录页面,显示错误信息 。此时
AuthenticationFailureHandler
的实现为SimpleUrlAuthenticationFailureHandler
。