跳到主要内容

持久化认证

QWen Max 中英对照 Persistence Persisting Authentication

用户首次请求受保护的资源时,系统会提示输入凭证。提示输入凭证最常见的方法之一是将用户重定向到登录页面。未经身份验证的用户请求受保护资源时,简化的 HTTP 交换过程可能如下所示:

示例 1. 未经身份验证的用户请求受保护的资源

GET / HTTP/1.1
Host: example.com
Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b
http
HTTP/1.1 302 Found
Location: /login
http

用户提交他们的用户名和密码。

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
http

在验证用户身份后,系统会为用户分配一个新的会话 ID,以防止会话固定攻击

HTTP/1.1 302 Found
Location: /
Set-Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8; Path=/; HttpOnly; SameSite=Lax
http

后续请求包括会话 cookie,该 cookie 用于在剩余的会话期间验证用户身份。

GET / HTTP/1.1
Host: example.com
Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8
http

SecurityContextRepository

在 Spring Security 中,用户的关联到未来的请求是通过 SecurityContextRepository 完成的。SecurityContextRepository 的默认实现是 DelegatingSecurityContextRepository,它委托给以下内容:

HttpSessionSecurityContextRepository

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

NullSecurityContextRepository

如果不想将 SecurityContextHttpSession 关联(例如,在使用 OAuth 进行身份验证时),NullSecurityContextRepository 是一个 SecurityContextRepository 的实现,它不执行任何操作。

RequestAttributeSecurityContextRepository

RequestAttributeSecurityContextRepositorySecurityContext 保存为请求属性,以确保在跨可能清除 SecurityContext 的分发类型发生的单个请求中 SecurityContext 是可用的。

例如,假设客户端发出请求,通过身份验证,然后发生错误。根据 servlet 容器实现的不同,该错误意味着已建立的任何 SecurityContext 都会被清除,然后进行错误调度。当错误调度时,没有建立 SecurityContext。这意味着除非以某种方式持久化 SecurityContext,否则错误页面无法使用 SecurityContext 进行授权或显示当前用户。

public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.securityContext((securityContext) -> securityContext
.securityContextRepository(new RequestAttributeSecurityContextRepository())
);
return http.build();
}
java

DelegatingSecurityContextRepository

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

最实用的配置如下例所示,该配置允许同时使用 RequestAttributeSecurityContextRepositoryHttpSessionSecurityContextRepository

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.securityContext((securityContext) -> securityContext
.securityContextRepository(new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository()
))
);
return http.build();
}
java
备注

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

SecurityContextPersistenceFilter

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

securitycontextpersistencefilter

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

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

3 最后,如果 SecurityContext 发生了变化,我们使用 SecurityContextRepository 保存 SecurityContext。这意味着当使用 SecurityContextPersistenceFilter 时,只需设置 SecurityContextHolder 就可以确保 SecurityContext 通过 SecurityContextRepository 持久化。

在某些情况下,响应在 SecurityContextPersistenceFilter 方法完成之前就已经提交并写入客户端。例如,如果向客户端发送了重定向,响应会立即写回客户端。这意味着在步骤 3 中建立 HttpSession 是不可能的,因为会话 ID 无法包含在已经写入的响应中。另一种可能发生的情况是,如果客户端成功通过身份验证,在 SecurityContextPersistenceFilter 完成之前响应就已经提交,并且在 SecurityContextPersistenceFilter 完成之前客户端发起了第二次请求。那么第二次请求中可能会出现错误的身份验证信息。

为了避免这些问题,SecurityContextPersistenceFilter 会包装 HttpServletRequestHttpServletResponse,以检测 SecurityContext 是否发生变化,如果发生变化,则在响应被提交之前保存 SecurityContext

SecurityContextHolderFilter

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

securitycontextholderfilter

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

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

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

public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.securityContext((securityContext) -> securityContext
.requireExplicitSave(true)
);
return http.build();
}
java

在使用该配置时,重要的是任何设置 SecurityContextHolder 的代码(使用 SecurityContext)如果需要在请求之间持久化 SecurityContext,也应将其保存到 SecurityContextRepository 中。

例如,以下代码:

SecurityContextHolder.setContext(securityContext);
java

应该被替换为

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