Authentication Persistence and Session Management
在你获取到 authenticating requests 的应用程序时,考虑将来请求中如何保留和恢复该授权结果非常重要。
默认情况下,此操作会自动完成,因此无需编写额外的代码,不过了解 HttpSecurity
中 requireExplicitSave
的含义非常重要。
如果愿意,可以使用 you can read more about what requireExplicitSave is doing 或 why it’s important。否则,大多数情况下您都已完成本部分内容。
但在结束之前,请考虑以下这些用例是否适合您的应用程序:
-
我想让 restrict the number of times 用户可以同时登录
-
我想由我自己而不是 Spring Security 来进行 to store the authentication directly
-
我将身份验证手动存储,并且我想 to remove it
-
我正在使用 <<`SessionManagementFilter`,the-sessionmanagementfilter>> 并且我需要 guidance on moving away from that
-
我想将身份验证存储在 in something other than the session
-
我正在使用 stateless authentication,但是 I’d still like to store it in the session
-
我正在使用
SessionCreationPolicy.NEVER
,但是 the application is still creating sessions。
Understanding Session Management’s Components
会话管理支持由几个组件组成,这些组件协同工作以提供该功能。这些组件是 the SecurityContextHolderFilter
、the SecurityContextPersistenceFilter
和 the SessionManagementFilter
。
在 Spring Security 6 中,默认情况下不会设置 |
The SessionManagementFilter
SessionManagementFilter
会将 SecurityContextRepository
的内容与 SecurityContextHolder
的当前内容进行比较,以确定用户是否在当前请求期间经过身份验证,通常是通过非交互式身份验证机制,例如预身份验证或记住我。这种身份验证机制在身份验证后执行重定向(例如表单登录),不会被 SessionManagementFilter
检测到,因为该过滤器不会在身份验证请求期间调用。在这些情况下,需要单独处理会话管理功能。如果该存储库包含安全上下文,则过滤器不会执行任何操作。如果它不包含,并且线程局部 SecurityContext
包含一个(非匿名)Authentication
对象,则该过滤器会认为它们已通过堆栈中的先前过滤器进行身份验证。然后,它将调用已配置的 SessionAuthenticationStrategy
。
如果用户当前未经身份验证,该过滤器会检查是否请求了无效的会话 ID(例如,由于超时),并且会调用已配置的 InvalidSessionStrategy
(如果已设置)。最常见的行为只是重定向到固定 URL,并且此行为封装在标准实现 SimpleRedirectInvalidSessionStrategy
中。在通过命名空间配置无效会话 URL 时,后者也会被使用, as described earlier。
Moving Away From SessionManagementFilter
在 Spring Security 5 中,默认配置依赖 SessionManagementFilter
来检测用户是否刚刚完成授权,并调用 {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[SessionAuthenticationStrategy
]。这样做的一个问题是,这表示在典型设置中,HttpSession
必须对每个请求进行读取。
在 Spring Security 6 中,默认情况下,身份验证机制本身必须调用 SessionAuthenticationStrategy
。这意味着无需检测 Authentication
何时完成,因此不必为每个请求读取 HttpSession
。
Things To Consider When Moving Away From SessionManagementFilter
在 Spring Security 6 中,默认情况下不使用 SessionManagementFilter
,因此来自 sessionManagement
DSL 的一些方法将不起作用。
Method | Replacement |
---|---|
|
在身份验证机制中配置一个 {security-api-url}/org/springframework/security/web/authentication/AuthenticationFailureHandler.html[ |
|
在身份验证机制中配置一个 {security-api-url}/org/springframework/security/web/authentication/AuthenticationFailureHandler.html[ |
|
在您的身份验证机制中将一个 |
如果您尝试使用这些方法中的任何一个,将抛出异常。
Customizing Where the Authentication Is Stored
默认情况下,Spring Security 为您将安全上下文存储在 HTTP 会话中。但是,这里有几个您可能需要自定义它的原因:
-
你可能想在
HttpSessionSecurityContextRepository
实例上调用单个 setter -
你可能希望将安全上下文存储在缓存或数据库中以实现横向扩展
首先,您需要创建一个 SecurityContextRepository
的实现或使用现有实现如 HttpSessionSecurityContextRepository
,然后可以将其设置在 HttpSecurity
中。
SecurityContextRepository
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
SecurityContextRepository repo = new MyCustomSecurityContextRepository();
http
// ...
.securityContext((context) -> context
.securityContextRepository(repo)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val repo = MyCustomSecurityContextRepository()
http {
// ...
securityContext {
securityContextRepository = repo
}
}
return http.build()
}
<http security-context-repository-ref="repo">
<!-- ... -->
</http>
<bean name="repo" class="com.example.MyCustomSecurityContextRepository" />
以上配置在 |
如果您使用的是自定义身份验证机制,您可能希望 store the Authentication
by yourself。
Storing the Authentication
manually
在某些情况下,例如,你可能手动授权用户,而不是依赖 Spring Security 过滤器。你可以使用一个自定义过滤器或 {spring-framework-reference-url}/web.html#mvc-controller[Spring MVC 控制器] 端点来执行此操作。如果你想在请求之间保存授权(例如,在 HttpSession
中),你必须执行以下操作:
-
Java
private SecurityContextRepository securityContextRepository =
new HttpSessionSecurityContextRepository(); 1
@PostMapping("/login")
public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) { 2
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
loginRequest.getUsername(), loginRequest.getPassword()); 3
Authentication authentication = authenticationManager.authenticate(token); 4
SecurityContext context = securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication); 5
securityContextHolderStrategy.setContext(context);
securityContextRepository.saveContext(context, request, response); 6
}
class LoginRequest {
private String username;
private String password;
// getters and setters
}
1 | 将 SecurityContextRepository 添加到控制器 |
2 | 注入 HttpServletRequest 和 HttpServletResponse 以便保存 SecurityContext |
3 | 使用提供的凭证创建未经验证的 UsernamePasswordAuthenticationToken |
4 | 调用 AuthenticationManager#authenticate 以验证用户 |
5 | 创建一个 SecurityContext 并设置其中的 Authentication |
6 | 将 SecurityContext 保存到 SecurityContextRepository 中 |
就是这样。如果您不确定上述示例中的 securityContextHolderStrategy
是什么,您可以在 Using SecurityContextStrategy
section 中阅读更多相关内容。
Properly Clearing an Authentication
如果你正在使用 Spring Security 的 Logout Support,那么它可以为你处理很多事情,包括清除和保存上下文。但是,假设你需要手动让用户注销你的应用,在这种情况下,你需要确保你 clearing and saving the context properly。
Configuring Persistence for Stateless Authentication
有时,不需要创建和维护 HttpSession
,例如为了对请求进行授权保留。一些授权机制(例如 HTTP Basic)是无状态的,因此,会在每个请求中对用户进行重新授权。
如果您不希望创建会话,您可以使用 SessionCreationPolicy.STATELESS
,如下所示:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.STATELESS
}
}
return http.build()
}
<http create-session="stateless">
<!-- ... -->
</http>
上述配置 configuring the SecurityContextRepository
使用 NullSecurityContextRepository
,并且还 preventing the request from being saved in the session。
如果你正在使用 SessionCreationPolicy.NEVER
,你可能会注意到应用程序仍在创建一个 HttpSession
。在大多数情况下,这样做是因为 request is saved in the session 需要在身份验证成功后重新请求已验证的资源。为了避免这种情况,请参阅 how to prevent the request of being saved 部分。
Storing Stateless Authentication in the Session
如果由于某种原因,您使用的是无状态身份验证机制,但您仍想将身份验证存储在会话中,您可以使用 HttpSessionSecurityContextRepository
而不是 NullSecurityContextRepository
。
对于 HTTP Basic,你可以添加 a ObjectPostProcessor
,它会更改 BasicAuthenticationFilter
所使用的 SecurityContextRepository
:
HttpSession
-
Java
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.httpBasic((basic) -> basic
.addObjectPostProcessor(new ObjectPostProcessor<BasicAuthenticationFilter>() {
@Override
public <O extends BasicAuthenticationFilter> O postProcess(O filter) {
filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
return filter;
}
})
);
return http.build();
}
上述内容同样适用于其他授权机制(例如 Bearer Token Authentication)。
Understanding Require Explicit Save
在 Spring Security 5 中,默认行为是通过 <<`SecurityContextPersistenceFilter`,securitycontextpersistencefilter>> 自动将 SecurityContext
保存到 SecurityContextRepository
。必须在提交 HttpServletResponse
之前且在 SecurityContextPersistenceFilter
之前执行保存。但是,当在请求完成之前(即在提交 HttpServletResponse
之前)自动保存 SecurityContext
时,可能会让用户感到意外。另外,跟踪该状态以确定是否需要保存也很复杂,这可能会在 SecurityContextRepository
(即 HttpSession
)进行不必要的写入。
由于这些原因,SecurityContextPersistenceFilter
已被弃用,取而代之的是 SecurityContextHolderFilter
。在 Spring Security 6 中,默认行为是 the SecurityContextHolderFilter
将只从 SecurityContextRepository
中读取 SecurityContext
并将其填充到 SecurityContextHolder
中。现在,如果用户希望 SecurityContext
在请求之间持续存在,他们必须使用 SecurityContextRepository
显式保存 SecurityContext
。这样消除了歧义,并且只在需要时写入 SecurityContextRepository
(即 HttpSession
),从而提高了性能。
How it works
简而言之,当 requireExplicitSave
为 true
时,Spring Security 设置 the SecurityContextHolderFilter
而不是 the SecurityContextPersistenceFilter
Configuring Concurrent Session Control
如果你希望限制某个用户登录到你的应用程序的能力,Spring Security 通过以下简单的附加项来开箱即用的支持此功能。首先,你需要向你的配置中添加以下侦听器,以使 Spring Security 了解会话生命周期事件:
-
Java
-
Kotlin
-
web.xml
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
open fun httpSessionEventPublisher(): HttpSessionEventPublisher {
return HttpSessionEventPublisher()
}
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
然后将以下行添加到你的安全配置中:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.maximumSessions(1)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
sessionConcurrency {
maximumSessions = 1
}
}
}
return http.build()
}
<http>
...
<session-management>
<concurrency-control max-sessions="1" />
</session-management>
</http>
这将防止用户多次登录——第二次登录会导致第一次登录无效。
使用 Spring Boot,你可以按以下方式测试上述配置方案:
-
Java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MaximumSessionsTests {
@Autowired
private MockMvc mvc;
@Test
void loginOnSecondLoginThenFirstSessionTerminated() throws Exception {
MvcResult mvcResult = this.mvc.perform(formLogin())
.andExpect(authenticated())
.andReturn();
MockHttpSession firstLoginSession = (MockHttpSession) mvcResult.getRequest().getSession();
this.mvc.perform(get("/").session(firstLoginSession))
.andExpect(authenticated());
this.mvc.perform(formLogin()).andExpect(authenticated());
// first session is terminated by second login
this.mvc.perform(get("/").session(firstLoginSession))
.andExpect(unauthenticated());
}
}
你可以使用 最多会话示例 来尝试此操作。
通常,你可能更愿意防止第二次登录,在这种情况下你可以使用:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
sessionConcurrency {
maximumSessions = 1
maxSessionsPreventsLogin = true
}
}
}
return http.build()
}
<http>
<session-management>
<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>
第二次登录将被拒绝。“拒绝”是指如果使用基于表单的登录,系统会将用户发送到 authentication-failure-url
。如果第二次身份验证通过另一个非交互机制(例如“记住我”)进行,系统会向客户端发送“未授权”(401)错误。如果你希望使用错误页面,则可以将属性 session-authentication-error-url
添加到 session-management
元素中。
使用 Spring Boot,你可以按以下方式测试上述配置:
-
Java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MaximumSessionsPreventLoginTests {
@Autowired
private MockMvc mvc;
@Test
void loginOnSecondLoginThenPreventLogin() throws Exception {
MvcResult mvcResult = this.mvc.perform(formLogin())
.andExpect(authenticated())
.andReturn();
MockHttpSession firstLoginSession = (MockHttpSession) mvcResult.getRequest().getSession();
this.mvc.perform(get("/").session(firstLoginSession))
.andExpect(authenticated());
// second login is prevented
this.mvc.perform(formLogin()).andExpect(unauthenticated());
// first session is still valid
this.mvc.perform(get("/").session(firstLoginSession))
.andExpect(authenticated());
}
}
如果你正在为基于表单的登录使用一个定制的授权过滤器,那么你必须显式配置并发会话控制支持。你可以使用 最多会话阻止登录示例 来尝试此操作。
Detecting Timeouts
会话本身会过期,无需采取措施确保删除安全上下文。也就是说,Spring Security 可以检测到何时会话过期,并采取你指示的特定操作。例如,当用户使用已过期的会话发出请求时,你可能希望重新定向到特定端点。这是通过 HttpSecurity
中的 invalidSessionUrl
实现的:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.invalidSessionUrl("/invalidSession")
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
invalidSessionUrl = "/invalidSession"
}
}
return http.build()
}
<http>
...
<session-management invalid-session-url="/invalidSession" />
</http>
请注意,如果你使用此机制来检测会话超时,如果用户注销然后再登录而不关闭浏览器,则可能会错误地报告错误。这是因为会话 cookie 在你使会话无效时不会被清除,即使用户已注销,也会被重新提交。如果是这种情况,你可能希望 configure logout to clear the session cookie。
Customizing the Invalid Session Strategy
invalidSessionUrl
是一个便捷方法,用于使用 {security-api-url}/org/springframework/security/web/session/SimpleRedirectInvalidSessionStrategy.html[ SimpleRedirectInvalidSessionStrategy
实现] 设置 InvalidSessionStrategy
。如果你想要定制此行为,那么你可以实现 {security-api-url}/org/springframework/security/web/session/InvalidSessionStrategy.html[InvalidSessionStrategy
] 接口,并使用 invalidSessionStrategy
方法对其进行配置:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.invalidSessionStrategy(new MyCustomInvalidSessionStrategy())
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
invalidSessionStrategy = MyCustomInvalidSessionStrategy()
}
}
return http.build()
}
<http>
...
<session-management invalid-session-strategy-ref="myCustomInvalidSessionStrategy" />
<bean name="myCustomInvalidSessionStrategy" class="com.example.MyCustomInvalidSessionStrategy" />
</http>
Clearing Session Cookies on Logout
你可以明确删除 JSESSIONID cookie,例如使用注销处理程序中的 link:https://w3c.github.io/webappsec-clear-site-data/[Clear-Site-Data
头:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.logout((logout) -> logout
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES)))
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
logout {
addLogoutHandler(HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(COOKIES)))
}
}
return http.build()
}
<http>
<logout success-handler-ref="clearSiteDataHandler" />
<b:bean id="clearSiteDataHandler" class="org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler">
<b:constructor-arg>
<b:bean class="org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter">
<b:constructor-arg>
<b:list>
<b:value>COOKIES</b:value>
</b:list>
</b:constructor-arg>
</b:bean>
</b:constructor-arg>
</b:bean>
</http>
这样做的好处是不依赖于容器,并且将适用于支持 Clear-Site-Data
头的任何容器。
作为替代,你还可以使用注销处理程序中的以下语法:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.logout(logout -> logout
.deleteCookies("JSESSIONID")
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
logout {
deleteCookies("JSESSIONID")
}
}
return http.build()
}
<http>
<logout delete-cookies="JSESSIONID" />
</http>
遗憾的是,无法保证这适用于每个 Servlet 容器,因此你需要在你的环境中进行测试。
如果你在代理后面运行你的应用程序,你也可以通过配置代理服务器来移除会话 cookie。例如,通过使用 Apache HTTPD 的 |
<LocationMatch "/tutorial/logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
</LocationMatch>
有关 Clear Site Data 和 Logout sections 的更多详细信息。
Understanding Session Fixation Attack Protection
Session fixation 攻击是一种潜在风险,恶意攻击者可以通过访问网站来创建一个会话,然后通过其他方式说服另一位用户使用相同的会话登录(例如,通过向他们发送一个包含会话标识符作为参数的链接)。Spring Security 会自动通过在用户登录时创建一个新会话或更改会话 ID 来防止此攻击。
Configuring Session Fixation Protection
你可以通过三个推荐选项来控制会话固定保护策略:
-
changeSessionId
- 不要创建新会话。相反,使用 Servlet 容器提供的会话固化保护 (HttpServletRequest#changeSessionId()
)。此选项仅适用于 Servlet 3.1 (Java EE 7) 及更高版本的容器。在旧版容器中指定它将导致异常。这是 Servlet 3.1 和更高版本容器中的默认设置。 -
newSession
- 创建一个新的“干净”会话,而不复制现有会话数据(与 Spring Security 相关的属性仍将被复制)。 -
migrateSession
- 创建一个新会话并将所有现有会话属性复制到新会话中。这是 Servlet 3.0 或更旧版本容器中的默认设置。
你可以通过下列方法配置会话修复防护:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement((session) -> session
.sessionFixation((sessionFixation) -> sessionFixation
.newSession()
)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
sessionFixation {
newSession()
}
}
}
return http.build()
}
<http>
<session-management session-fixation-protection="newSession" />
</http>
会话修复保护发生时,会发布 SessionFixationProtectionEvent
并将其发布到应用程序上下文中。如果你使用 changeSessionId
,此保护将 also 使任何 jakarta.servlet.http.HttpSessionIdListener
都收到通知,因此,如果你的代码同时侦听两个事件,那么请谨慎使用。
您也可以设置会话固定保护为 1 以禁用它,但这是不推荐的,因为它会使您的应用程序容易受到攻击。
Using SecurityContextHolderStrategy
考虑以下代码块:
-
Java
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), loginRequest.getPassword());
Authentication authentication = this.authenticationManager.authenticate(token);
// ...
SecurityContext context = SecurityContextHolder.createEmptyContext(); 1
context.setAuthentication(authentication); 2
SecurityContextHolder.setContext(context); 3
-
通过静态访问
SecurityContextHolder
来创建空SecurityContext
实例。 -
设置
SecurityContext
实例中的Authentication
对象。 -
静态设置
SecurityContextHolder
中的SecurityContext
实例。
虽然上面的代码运行良好,但它会产生一些不希望的影响:当组件通过 statically 通过访问 SecurityContext
SecurityContextHolder
时,当存在多个想要指定 SecurityContextHolderStrategy
的应用程序上下文时,这会创建竞争条件。这是因为在 SecurityContextHolder
中,每个类加载器只有一个策略,而不是每个应用程序上下文一个策略。
为了解决此问题,组件可以从应用程序上下文连接 SecurityContextHolderStrategy
。默认情况下,它们仍然会从 SecurityContextHolder
查找策略。
这些更改在很大程度上是内部的,但它们为应用程序提供了自动连接 SecurityContextHolderStrategy
的机会,而不是访问 SecurityContext
statically。为此,您应该将代码更改为以下内容:
-
Java
public class SomeClass {
private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
public void someMethod() {
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
loginRequest.getUsername(), loginRequest.getPassword());
Authentication authentication = this.authenticationManager.authenticate(token);
// ...
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); 1
context.setAuthentication(authentication); 2
this.securityContextHolderStrategy.setContext(context); 3
}
}
-
使用已配置的
SecurityContextHolderStrategy
创建一个空SecurityContext
实例。 -
设置
SecurityContext
实例中的Authentication
对象。 -
在
SecurityContextHolderStrategy
中设置SecurityContext
实例。
Forcing Eager Session Creation
有时,迫切创建会话会很有价值。这可以通过使用 {security-api-url}org/springframework/security/web/session/ForceEagerSessionCreationFilter.html[ForceEagerSessionCreationFilter
] 来完成,该过滤器可以使用以下配置:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.ALWAYS
}
}
return http.build()
}
<http create-session="ALWAYS">
</http>
What to read next
-
具有 Spring Session 的集群会话