Persisting Authentication
用户第一次请求受保护的资源时,系统会提示他们输入凭据。提示凭据的最常见方法之一是将用户重定向到登录页。对于请求受保护资源的未经身份验证的用户,总结的 HTTP 交换可能如下所示:
Example 1. Unauthenticated User Requests Protected Resource
GET / HTTP/1.1
Host: example.com
Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b
HTTP/1.1 302 Found
Location: /login
用户提交他们的用户名和密码。
Username and Password Submitted
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 相关联,以防止会话固定攻击。
Authenticated User is Associated to New Session
HTTP/1.1 302 Found
Location: /
Set-Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8; Path=/; HttpOnly; SameSite=Lax
后续请求包括会话 Cookie,该 Cookie 用于在会话的其余部分对用户进行身份验证。
Authenticated Session Provided as Credentials
GET / HTTP/1.1
Host: example.com
Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8
SecurityContextRepository
在 SpringSecurity 中,用户与未来请求的关联是使用 SecurityContextRepository 实现的。
SecurityContextRepository 的默认实现是 RegiatingSecurityContextRepository,它将委托给以下内容:
HttpSessionSecurityContextRepository
RequestAttributeSecurityContextRepository
HttpSessionSecurityContextRepository
HttpSessionSecurityContextRepository 将 SecurityContext 与 HttpSession 关联。用户可以用 SecurityContextRepository 的另一个实现替换 HttpSessionSecurityContextRepository,如果他们希望以另一种方式将用户与后续请求关联或根本不关联的话。
NullSecurityContextRepository
如果不希望将 SecurityContext 关联到一个 HttpSession (即在使用 OAuth 进行身份验证时) ,则 NullSecurityContextRepository 是 SecurityContextRepository 的一个实现,它不执行任何操作。
RequestAttributeSecurityContextRepository
RequestAttributeSecurityContextRepository 将 SecurityContext 保存为一个请求属性,以确保对于可能清除 SecurityContext 的分派类型发生的单个请求,SecurityContext 是可用的。
例如,假设客户端发出请求,经过身份验证,然后出现错误。根据 servlet 容器实现,错误意味着清除建立的任何 SecurityContext,然后进行错误分派。当进行错误分派时,不会建立 SecurityContext。这意味着除非以某种方式持久化 SecurityContext,否则错误页面不能使用 SecurityContext 进行授权或显示当前用户。
Use RequestAttributeSecurityContextRepository
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.securityContext((securityContext) -> securityContext
.securityContextRepository(new RequestAttributeSecurityContextRepository())
);
return http.build();
}
DelegatingSecurityContextRepository
RegiatingSecurityContextRepository 将 SecurityContext 保存到多个 SecurityContextRepository 委托,并允许以指定的顺序从任何委托中检索。
对此最有用的安排是使用以下示例配置的,该示例允许同时使用 RequestAttributeSecurityContextRepository 和 HttpSessionSecurityContextRepository。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.securityContext((securityContext) -> securityContext
.securityContextRepository(new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository()
))
);
return http.build();
}
在 Spring Security6中,上面显示的示例是默认配置。
SecurityContextPersistenceFilter
SecurityContextPersisenceFilter 负责在使用 SecurityContextRepository 的请求之间持久化 SecurityContext。
- 在运行应用程序的其余部分之前,SecurityContextPersisenceFilter 从 SecurityContextRepository 加载 SecurityContext 并将其设置为 SecurityContextHolder。
- 接下来,运行应用程序。
- 最后,如果 SecurityContext 已经更改,我们将使用 SecurityContextPersisenceRepository 保存 SecurityContext。这意味着在使用 SecurityContextPersisenceFilter 时,只需设置 SecurityContextHolder 即可确保使用 SecurityContextRepository 保持 SecurityContext。
在某些情况下,在 SecurityContextPersisenceFilter 方法完成之前,将响应提交并写入客户端。例如,如果将重定向发送到客户端,则会立即将响应写回到客户端。这意味着在步骤3中不可能建立 HttpSession,因为会话 id 不能包含在已经写好的响应中。另一种可能发生的情况是,如果客户端成功认证,响应在 SecurityContextPersistenceFilter 完成之前就已经提交,并且客户端在 SecurityContextPersistenceFilter 完成之前发出第二个请求,那么第二个请求中可能会出现错误的认证信息。
为了避免这些问题,SecurityContextPersisenceFilter 同时包装了 HttpServletRequest 和 HttpServletResponse,以检测 SecurityContext 是否已经更改,如果已经更改,则在响应提交之前保存 SecurityContext。
SecurityContextHolderFilter
SecurityContextHolderFilter 负责在使用 SecurityContextRepository 的请求之间加载 SecurityContext。
- 在运行应用程序的其余部分之前,SecurityContextHolderFilter 从 SecurityContextRepository 加载 SecurityContext 并将其设置为 SecurityContextHolder。
- 接下来,运行应用程序。
与 SecurityContextPersisenceFilter 不同,SecurityContextHolderFilter 只加载SecurityContext,它不保存 SecurityContext。
Explicit Saving of SecurityContext
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.securityContext((securityContext) -> securityContext
.requireExplicitSave(true)
);
return http.build();
}
在使用配置时,重要的是,任何用 SecurityContext 设置 SecurityContextHolder 的代码都应该将 SecurityContext 保存到 SecurityContextRepository,如果它应该在请求之间保存的话。
例如,以下代码:
使用 SecurityContextPersisenceFilter 设置 SecurityContextHolder
SecurityContextHolder.setContext(securityContext);
应该用
使用 SecurityContextHolderFilter 设置 SecurityContextHolder
SecurityContextHolder.setContext(securityContext);
securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse);