SpringSecurity系列——持久认证(Persisting Authentication),会话管理day3-2(源于官网5.7.2版本)
前言
源于官方最新5.7.2文档,若你觉得官方文档阅读起来很枯燥,内容复杂,我提供了解析概括在每个部分的结尾,我对官方文档的内容做了一些改变,实例代码我会在后续进行更新,请查看如:SpringSecurity系列——认证架构实例代码的文章
但是如果你有能力,还是推荐直接阅读官方文档
持久认证(Persisting Authentication)
用户第一次请求受保护的资源时,系统会提示他们输入凭据。 提示输入凭据的最常见方法之一是将用户重定向到登录页面。 请求受保护资源的未经身份验证的用户的汇总 HTTP 交换可能如下所示:
这里的302表示重定向
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
//经过身份验证的用户与新会话相关联
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 进行的。
HttpSecurityContextRepository(Http安全上下文存储库)
SecurityContextRepository 的默认实现是 HttpSessionSecurityContextRepository
,它将 SecurityContext 与 HttpSession 相关联。 如果用户希望以其他方式将用户与后续请求关联或根本不关联,则用户可以将 HttpSessionSecurityContextRepository
替换为 SecurityContextRepository 的另一个实现。
NullSecurityContextRepository(空安全上下文存储库)
如果不希望将 SecurityContext 与 HttpSession 相关联(即使用 OAuth 进行身份验证时),则 NullSecurityContextRepository 是 SecurityContextRepository 的实现,它什么都不做。
RequestAttributeSecurityContextRepository(请求属性安全上下文存储库)
RequestAttributeSecurityContextRepository 将 SecurityContext 保存为请求属性,以确保 SecurityContext 可用于跨越可能清除 SecurityContext 的调度类型发生的单个请求。
例如,假设客户端发出请求,经过身份验证,然后发生错误。 根据 servlet 容器实现,错误意味着已建立的任何 SecurityContext 都被清除,然后进行错误分派。 进行错误分派时,没有建立 SecurityContext。 这意味着错误页面不能使用 SecurityContext 进行授权或显示当前用户,除非 SecurityContext 以某种方式持久化。
示例 5. 使用 RequestAttributeSecurityContextRepository
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.securityContext((securityContext) -> securityContext
.securityContextRepository(new RequestAttributeSecurityContextRepository())
);
return http.build();
}
SecurityContextPersistenceFilter(安全上下文持久化过滤器)
SecurityContextPersistenceFilter 负责使用 SecurityContextRepository 在请求之间持久化 SecurityContext。
- 在运行应用程序的其余部分之前,SecurityContextPersistenceFilter 从 SecurityContextRepository 加载 SecurityContext 并将其设置在 SecurityContextHolder 上。
- 接下来,运行应用程序。
- 最后,如果 SecurityContext 已更改,我们使用 SecurityContextPersistenceRepository 保存 SecurityContext。 这意味着在使用 SecurityContextPersistenceFilter 时,只需设置 SecurityContextHolder 即可确保使用 SecurityContextRepository 持久化 SecurityContext。
在某些情况下,响应会在 SecurityContextPersisteneFilter 方法完成之前提交并写入客户端。 例如,如果向客户端发送重定向,则响应会立即写回客户端。 这意味着在第 3 步中无法建立 HttpSession,因为会话 ID 无法包含在已写入的响应中。 另一种可能发生的情况是,如果客户端成功验证,则在 SecurityContextPersistenceFilter 完成之前提交响应,并且客户端在 SecurityContextPersistenceFilter 完成之前发出第二个请求,错误的验证可能出现在第二个请求中。
为了避免这些问题,SecurityContextPersistenceFilter 包装了 HttpServletRequest 和 HttpServletResponse 以检测 SecurityContext 是否已更改,如果已更改,则在提交响应之前保存 SecurityContext。
SecurityContextHolderFilter(安全上下文持有者过滤器)
SecurityContextHolderFilter 负责使用 SecurityContextRepository 在请求之间加载 SecurityContext。
- 在运行应用程序的其余部分之前,SecurityContextHolderFilter 从 SecurityContextRepository 加载 SecurityContext 并将其设置在 SecurityContextHolder 上。
- 接下来,运行应用程序。
与 SecurityContextPersisteneFilter 不同,SecurityContextHolderFilter 只加载 SecurityContext 而不会保存 SecurityContext。 这意味着在使用 SecurityContextHolderFilter 时,需要显式保存 SecurityContext。
显式保存 SecurityContext
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.securityContext((securityContext) -> securityContext
.requireExplicitSave(true)
);
return http.build();
}
解释概括
- 用户第一次请求受保护资源时会触发重定向至认证登录的界面
- SecurityContextRepository 的默认实现是 HttpSecurityContextRepository,将 SecurityContext 与 HttpSession 相关联,若不想实现可以使用NullSecurityContextRepository
- RequestAttributeSecurityContextRepository将 SecurityContext 保存为请求属性,使用SecurityContextPersistenceFilter 负责使用 SecurityContextRepository 在请求之间持久化 SecurityContext,以确保 SecurityContext 可用于跨越可能清除 SecurityContext 的调度类型发生的单个请求
- 使用 SecurityContextPersistenceFilter 时,只需设置 SecurityContextHolder 即可确保使用 SecurityContextRepository 持久化 SecurityContext
- SecurityContextHolderFilter 只加载 SecurityContext 而不会保存 SecurityContext,所以需要我们自己进行显式保存
Session Management(会话管理)
Force Eager Session Creation(强制急切会话创建)
有时,急切地创建会话可能很有价值。 这可以通过使用可以使用配置的 ForceEagerSessionCreationFilter 来完成
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
);
return http.build();
}
Detecting Timeouts(检测超时)
您可以配置 Spring Security 以检测无效会话 ID 的提交并将用户重定向到适当的 URL。 这是通过会话管理元素实现的:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.invalidSessionUrl("/invalidSession.htm")
);
return http.build();
}
请注意,如果您使用此机制来检测会话超时,如果用户注销然后重新登录而不关闭浏览器,它可能会错误地报告错误。 这是因为当您使会话无效时会话 cookie 不会被清除,即使用户已注销也会重新提交。 您可以在注销时显式删除 JSESSIONID cookie,例如在注销处理程序中使用以下语法:
但这不能保证适用于每个 servlet 容器
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.logout(logout -> logout
.deleteCookies("JSESSIONID")
);
return http.build();
}
Concurrent Session Control(并发会话控制)
如果您希望限制单个用户登录到您的应用程序的能力,Spring Security 通过以下简单的补充支持开箱即用。 首先,您需要将以下侦听器添加到您的配置中,以使 Spring Security 更新有关会话生命周期事件
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
然后添加以下内容
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.maximumSessions(1)
);
return http.build();
}
这将阻止用户多次登录 - 第二次登录将导致第一次登录无效。 通常您希望阻止第二次登录,在这种情况下您可以使用
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
);
return http.build();
}
第二次登录将被拒绝。 “拒绝”是指如果使用基于表单的登录,用户将被发送到 authentication-failure-url。 如果通过另一种非交互机制进行第二次身份验证,例如“remember-me”,则会向客户端发送“unauthorized”(401)错误。 如果您想使用错误页面,则可以将属性 session-authentication-error-url 添加到 session-management 元素。
如果您为基于表单的登录使用定制的身份验证过滤器,则必须显式配置并发会话控制支持。