Spring Security 6.x 系列(11)—— Form表单认证流程

2023-12-18 18:49:42

一、前言

在本系列文章中介绍了过滤器和相关认证组件,对部分源码也进行详细分析。

本章主要学习 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";
}

三、认证流程分析

3.1 重定向登录页

上文配置所有 http 请求必须经过认证,在未登录时访问 /private 接口,会重定向登录页,流程如下:

在这里插入图片描述

① 首先,用户向未授权的资源 /private 发出未经身份认证的请求。

Spring SecurityAuthorizationFilter 抛出 AccessDeniedException 异常。

③ 由于用户未经过身份验证,因此 ExceptionTranslationFilter 将启动“启动身份验证”,并使用配置的 AuthenticationEntryPoint 将重定向发送到登录页。 在大多数情况下,是 LoginUrlAuthenticationEntryPoint 的实例。

④ 浏览器请求重定向到的登录页面。

⑤ 呈现默认登录页面。

下面我们会针对上述流程进行详细的源码分析。

3.1.1 抛出 AccessDeniedException 异常

/private 发出未经身份认证的请求会依次通过下述所有过滤器:

Security filter chain: [
  DisableEncodeUrlFilter
  WebAsyncManagerIntegrationFilter
  SecurityContextHolderFilter
  HeaderWriterFilter
  CsrfFilter
  LogoutFilter
  UsernamePasswordAuthenticationFilter
  DefaultLoginPageGeneratingFilter
  DefaultLogoutPageGeneratingFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  ExceptionTranslationFilter
  AuthorizationFilter
]

在进入最后一个过滤器 AuthorizationFilter 时,会对当前请求做最后的权限效验,如何未被授权,会抛出 AccessDeniedException 异常:

在这里插入图片描述
首先 AuthorizationFilter 会取出当前用户认证信息,因当前请求未经身份认证,获取的 AuthenticationAnonymousAuthenticationFilter 创建的匿名用户

在这里插入图片描述
接着使用 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 valid
	SecurityContext 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@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]

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@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 不再执行后续操作,最后登录页面效果:

在这里插入图片描述

3.2 表单登录

提交用户名和密码后,将对用户名和密码进行身份验:

在这里插入图片描述
①当用户输入用户名和密码提交登录,登录请求会被 UsernamePasswordAuthenticationFilter 处理,预构建认证对象 UsernamePasswordAuthenticationToken(未认证) 。

②接下来调用 AuthenticationManager 进行认证。

ProviderManagerAuthenticationManager 的实现类,ProviderManager 遍历所有的认证提供者,DaoAuthenticationProvider 符合 Form 表单认证。

③如果身份认证失败:

  • 清理 SecurityContextHolder

  • RememberMeServices#loginFail被调用。 如果未配置“记住我”,则为空操作。

  • AuthenticationFailureHandler 被调用,重定向 /login?error 登录页面,显示错误信息 。

④如果身份验证成功:

  • SessionAuthenticationStrategy 会话处理。

  • SecurityContextHolder 上存储认证信息。

  • RememberMeServices#loginSuccess 被调用。 如果未配置“记住我”,则为空操作。

  • ApplicationEventPublisher#InteractiveAuthenticationSuccessEvent 被调用发布认证成功事件。

  • AuthenticationSuccessHandler被调用,重定向登录前URL

3.2.1 进入 AbstractAuthenticationProcessingFilter

表单登录时,登录请求会进入到 UsernamePasswordAuthenticationFilter ,该过滤器会拦截浏览器提交的表单登录请求并进行身份认证。

UsernamePasswordAuthenticationFilter 继承自父类 AbstractAuthenticationProcessingFilterUsernamePasswordAuthenticationFilter 没有对父类的 AbstractAuthenticationProcessingFilterdoFilter 方法进行重写,故实际执行的是父类 AbstractAuthenticationProcessingFilterdoFilter 方法。

采用模版模式,根据不同的认证方式,执行不同子类的认证逻辑。

AbstractAuthenticationProcessingFilterdoFilter 方法几乎完成认证的所有流程:

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

3.2.2 进入 UsernamePasswordAuthenticationFilter

随后进入 UsernamePasswordAuthenticationFilterattemptAuthentication 方法,该方法会预构建认证对象 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 刚创建时,包含输入的用户名、密码、客户端IPsessionID 等信息,这时是未认证状态:

在这里插入图片描述

ProviderManagerAuthenticationManager 的实现类,ProviderManager 遍历所有的认证提供者,DaoAuthenticationProvider 符合 Form 表单认证,调用其 authenticate 方法进行认证。

在这里插入图片描述

3.2.3 进入 DaoAuthenticationProvider

UsernamePasswordAuthenticationToken 类型的 Authentication 对象是由 DaoAuthenticationProvider 进行认证处理 ,和上文的 AbstractAuthenticationProcessingFilter 类似,也是采用模版模式,首先调用的是父类 AbstractUserDetailsAuthenticationProviderauthenticate 方法:

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

其中有两个比较重要的点:

  • 调用子类 DaoAuthenticationProviderretrieveUser 方法,根据用户名获取用户信息
  • 调用子类 DaoAuthenticationProvideradditionalAuthenticationChecks 方法,校验密码

在校验密码成功后,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.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 认证失败处理

如果认证失败,比如密码错误,在 AbstractUserDetailsAuthenticationProviderauthenticate 方法中抛出以下异常信息:

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

在这里插入图片描述

文章来源:https://blog.csdn.net/ctwy291314/article/details/135061291
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。