在本系列文章中介绍了过滤器和相关认证组件,对部分源码也进行详细分析。
本章主要学习 Spring Security
中通过 HTML
表单提供用户名和密码的认证流程。
默认情况下,
Spring Security
表单登录处于启用状态。 但是,一旦提供了任何基于servlet
的配置,就必须显式提供基于表单的登录。
一个最小的显式 Java
配置示例:
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
/**
* 构建SecurityFilterChain
*
* @param http
* @return
* @throws Exception
*/
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 配置所有http请求必须经过认证
http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated());
// 开启表单认证(默认配置)
http.formLogin(Customizer.withDefaults());
// 构造器构建SecurityFilterChain对象
return http.build();
}
/**
* 配置登录名密码
*
* @return
*/
@Bean
public 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";
}
上文配置所有 http
请求必须经过认证,在未登录时访问 /private
接口,会重定向登录页,流程如下:
① 首先,用户向未授权的资源 /private
发出未经身份认证的请求。
② Spring Security
的 AuthorizationFilter
抛出 AccessDeniedException
异常。
③ 由于用户未经过身份验证,因此 ExceptionTranslationFilter
将启动“启动身份验证”,并使用配置的 AuthenticationEntryPoint
将重定向发送到登录页。 在大多数情况下,是 LoginUrlAuthenticationEntryPoint
的实例。
④ 浏览器请求重定向到的登录页面。
⑤ 呈现默认登录页面。
下面我们会针对上述流程进行详细的源码分析。
/private
发出未经身份认证的请求会依次通过下述所有过滤器:
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextHolderFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
ExceptionTranslationFilter
AuthorizationFilter
]
在进入最后一个过滤器 AuthorizationFilter
时,会对当前请求做最后的权限效验,如何未被授权,会抛出 AccessDeniedException
异常:
首先 AuthorizationFilter
会取出当前用户认证信息,因当前请求未经身份认证,获取的 Authentication
为 AnonymousAuthenticationFilter
创建的匿名用户:
接着使用 authorizationManager
授权管理器对当前认证信息进行检查,因为是匿名用户,所有判定当前请求无权访问,抛出 AccessDeniedException
异常:
抛出 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 valid
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
this.securityContextHolderStrategy.setContext(context);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
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@1299acf6
servletPath:/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=6A5FA61657FA3C84115F42CBF6CE370B
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextHolderFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
ExceptionTranslationFilter
AuthorizationFilter
]
************************************************************
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 denied
org.springframework.security.access.AccessDeniedException: Access Denied
at 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]
重定向后浏览器地址变为: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@13754d1b
servletPath:/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=6A5FA61657FA3C84115F42CBF6CE370B
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextHolderFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
ExceptionTranslationFilter
AuthorizationFilter
]
************************************************************
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
不再执行后续操作,最后登录页面效果:
提交用户名和密码后,将对用户名和密码进行身份验:
①当用户输入用户名和密码提交登录,登录请求会被 UsernamePasswordAuthenticationFilter
处理,预构建认证对象 UsernamePasswordAuthenticationToken
(未认证) 。
②接下来调用 AuthenticationManager
进行认证。
ProviderManager
是AuthenticationManager
的实现类,ProviderManager
遍历所有的认证提供者,DaoAuthenticationProvider
符合Form
表单认证。
③如果身份认证失败:
清理 SecurityContextHolder
。
RememberMeServices#loginFail
被调用。 如果未配置“记住我”,则为空操作。
AuthenticationFailureHandler
被调用,重定向 /login?error
登录页面,显示错误信息 。
④如果身份验证成功:
SessionAuthenticationStrategy
会话处理。
在 SecurityContextHolder
上存储认证信息。
RememberMeServices#loginSuccess
被调用。 如果未配置“记住我”,则为空操作。
ApplicationEventPublisher#InteractiveAuthenticationSuccessEvent
被调用发布认证成功事件。
AuthenticationSuccessHandler
被调用,重定向登录前URL
。
表单登录时,登录请求会进入到 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 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);
}
}
/**
* 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);
}
随后进入 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" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
UsernamePasswordAuthenticationToken
刚创建时,包含输入的用户名、密码、客户端IP
、sessionID
等信息,这时是未认证状态:
ProviderManager
是 AuthenticationManager
的实现类,ProviderManager
遍历所有的认证提供者,DaoAuthenticationProvider
符合 Form
表单认证,调用其 authenticate
方法进行认证。
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 details
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
回到 3.2.1 中的 doFilter
方法进行认证成功的后续处理:
CsrfAuthenticationStrategy
:它负责在执行认证请求之后,删除旧的令牌生成新的,确保每次请求之后,csrf-token
都得到更新。ChangeSessionIdAuthenticationStrategy
:主要是使用HttpServletRequest.changeSessionId()
方法修改sessionID
来防止会话固定攻击。会话处理完成后,调用 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
。
如果认证失败,比如密码错误,在 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
。