持久性身份验证

用户第一次请求受保护的资源时,他们会 提示输入凭据。提示输入凭据最常见的方法之一是将用户重定向到 登录页面。未经身份验证的用户请求受保护资源的 HTTP 交换摘要可能如下所示

示例 1. 未经身份验证的用户请求受保护的资源
GET / HTTP/1.1
Host: example.com
Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b
HTTP/1.1 302 Found
Location: /login

用户提交其用户名和密码。

提交用户名和密码
POST /login HTTP/1.1
Host: example.com
Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b

username=user&password=password&_csrf=35942e65-a172-4cd4-a1d4-d16a51147b3e

在对用户进行身份验证后,用户将与新的会话 ID 关联,以防止 会话固定攻击

已对经过身份验证的用户关联新的会话
HTTP/1.1 302 Found
Location: /
Set-Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8; Path=/; HttpOnly; SameSite=Lax

后续请求包括会话 Cookie,该 Cookie 用于在会话的剩余时间内对用户进行身份验证。

已提供经过身份验证的会话作为凭据
GET / HTTP/1.1
Host: example.com
Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8

SecurityContextRepository

在 Spring Security 中,使用 SecurityContextRepository 将用户与将来的请求关联。SecurityContextRepository 的默认实现是 DelegatingSecurityContextRepository,它委派给以下内容

HttpSessionSecurityContextRepository

HttpSessionSecurityContextRepositorySecurityContextHttpSession 关联。如果用户希望以其他方式或根本不将用户与后续请求关联,则可以使用 SecurityContextRepository 的另一个实现来替换 HttpSessionSecurityContextRepository

NullSecurityContextRepository

如果不需要将 SecurityContextHttpSession 关联(即使用 OAuth 进行身份验证时),则 NullSecurityContextRepositorySecurityContextRepository 的一个实现,它什么也不做。

RequestAttributeSecurityContextRepository

RequestAttributeSecurityContextRepositorySecurityContext 保存为请求属性,以确保 SecurityContext 可用于跨越可能清除 SecurityContext 的分派类型的单个请求。

例如,假设客户端发出请求、通过身份验证,然后出现错误。根据 Servlet 容器实现,错误意味着已建立的任何 SecurityContext 都将被清除,然后进行错误分派。当进行错误分派时,没有建立 SecurityContext。这意味着错误页面无法使用 SecurityContext 进行授权或显示当前用户,除非以某种方式保留 SecurityContext

使用 RequestAttributeSecurityContextRepository
  • Java

  • XML

public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		// ...
		.securityContext((securityContext) -> securityContext
			.securityContextRepository(new RequestAttributeSecurityContextRepository())
		);
	return http.build();
}
<http security-context-repository-ref="contextRepository">
	<!-- ... -->
</http>
<b:bean name="contextRepository"
	class="org.springframework.security.web.context.RequestAttributeSecurityContextRepository" />

DelegatingSecurityContextRepository

DelegatingSecurityContextRepositorySecurityContext 保存到多个 SecurityContextRepository 委托,并允许按指定顺序从任何委托中检索。

最实用的安排是使用以下示例进行配置,该示例允许同时使用 RequestAttributeSecurityContextRepositoryHttpSessionSecurityContextRepository

配置 DelegatingSecurityContextRepository
  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
		// ...
		.securityContext((securityContext) -> securityContext
			.securityContextRepository(new DelegatingSecurityContextRepository(
				new RequestAttributeSecurityContextRepository(),
				new HttpSessionSecurityContextRepository()
			))
		);
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
	http {
		// ...
		securityContext {
			securityContextRepository = DelegatingSecurityContextRepository(
				RequestAttributeSecurityContextRepository(),
				HttpSessionSecurityContextRepository()
			)
		}
	}
	return http.build()
}
<http security-context-repository-ref="contextRepository">
	<!-- ... -->
</http>
<bean name="contextRepository"
	class="org.springframework.security.web.context.DelegatingSecurityContextRepository">
		<constructor-arg>
			<bean class="org.springframework.security.web.context.RequestAttributeSecurityContextRepository" />
		</constructor-arg>
		<constructor-arg>
			<bean class="org.springframework.security.web.context.HttpSessionSecurityContextRepository" />
		</constructor-arg>
</bean>

在 Spring Security 6 中,上面显示的示例是默认配置。

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter 负责使用 SecurityContextRepository 在请求之间持久化 SecurityContext

securitycontextpersistencefilter

number 1 在运行应用程序的其余部分之前,SecurityContextPersistenceFilterSecurityContextRepository 加载 SecurityContext 并将其设置在 SecurityContextHolder 上。

number 2 接下来,运行应用程序。

number 3 最后,如果 SecurityContext 已更改,我们将使用 SecurityContextPersistenceRepository 保存 SecurityContext。这意味着在使用 SecurityContextPersistenceFilter 时,只需设置 SecurityContextHolder 即可确保使用 SecurityContextRepository 持久化 SecurityContext

在某些情况下,在 SecurityContextPersistenceFilter 方法完成之前,响应已提交并写入客户端。例如,如果将重定向发送到客户端,则响应会立即写回客户端。这意味着在步骤 3 中无法建立 HttpSession,因为会话 ID 无法包含在已写入的响应中。可能发生的另一种情况是,如果客户端成功验证,则在 SecurityContextPersistenceFilter 完成之前提交响应,并且客户端在 SecurityContextPersistenceFilter 完成之前发出第二个请求,则第二个请求中可能存在错误的验证。

为了避免这些问题,SecurityContextPersistenceFilter 封装了 HttpServletRequestHttpServletResponse,以检测 SecurityContext 是否已更改,如果已更改,则在提交响应之前保存 SecurityContext

SecurityContextHolderFilter

SecurityContextHolderFilter 负责使用 SecurityContextRepository 在请求之间加载 SecurityContext

securitycontextholderfilter

number 1 在运行应用程序的其余部分之前,SecurityContextHolderFilterSecurityContextRepository 加载 SecurityContext 并将其设置在 SecurityContextHolder 上。

number 2 接下来,运行应用程序。

SecurityContextPersistenceFilter 不同,SecurityContextHolderFilter 仅加载 SecurityContext,而不保存 SecurityContext。这意味着在使用 SecurityContextHolderFilter 时,需要显式保存 SecurityContext

显式保存 SecurityContext
  • Java

  • Kotlin

  • XML

public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		// ...
		.securityContext((securityContext) -> securityContext
			.requireExplicitSave(true)
		);
	return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
    http {
        securityContext {
            requireExplicitSave = true
        }
    }
    return http.build()
}
<http security-context-explicit-save="true">
	<!-- ... -->
</http>

在使用该配置时,重要的是任何使用 SecurityContext 设置 SecurityContextHolder 的代码还应将 SecurityContext 保存到 SecurityContextRepository,如果它应该在请求之间持久保存。

例如,以下代码

使用 SecurityContextPersistenceFilter 设置 SecurityContextHolder
  • Java

  • Kotlin

SecurityContextHolder.setContext(securityContext);
SecurityContextHolder.setContext(securityContext)

应替换为

使用 SecurityContextHolderFilter 设置 SecurityContextHolder
  • Java

  • Kotlin

SecurityContextHolder.setContext(securityContext);
securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse);
SecurityContextHolder.setContext(securityContext)
securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse)