身份验证持久化与会话管理
一旦你拥有了一个能够验证请求的应用程序,接下来重要的是考虑如何持久化并恢复该验证结果,以便在未来的请求中使用。
默认情况下,此操作会自动完成,因此无需额外代码,但了解 HttpSecurity 中 requireExplicitSave 的含义非常重要。
如果你愿意,可以进一步了解 requireExplicitSave 的作用 或 其重要性。否则,在大多数情况下,本节内容已经完成。
但在离开之前,请考虑以下应用场景是否适用于您的项目:
-
我想限制用户可以同时登录的次数
-
我想自己直接存储认证信息,而不是让 Spring Security 为我处理
-
我正在手动存储认证信息,并且我想移除它
-
我正在使用 SessionManagementFilter,并且我需要关于迁移的指导
-
我想将认证信息存储在会话以外的其他地方
-
我正在使用无状态认证,但我仍然想将其存储在会话中
-
我正在使用
SessionCreationPolicy.NEVER,但应用程序仍在创建会话。
理解会话管理的组成部分
会话管理支持由多个协同工作的组件构成,以提供相应功能。这些组件包括:SecurityContextHolderFilter、SecurityContextPersistenceFilter 以及 SessionManagementFilter。
在 Spring Security 6 中,默认不再配置 SecurityContextPersistenceFilter 和 SessionManagementFilter。此外,任何应用程序应仅配置 SecurityContextHolderFilter 或 SecurityContextPersistenceFilter 中的一个,切勿同时配置两者。
SessionManagementFilter
SessionManagementFilter 会检查 SecurityContextRepository 中的内容与 SecurityContextHolder 的当前内容,以确定用户是否在当前请求期间(通常通过非交互式认证机制,例如预认证或记住我功能[1])已被认证。如果存储库中包含安全上下文,该过滤器将不执行任何操作。如果存储库中不包含安全上下文,且线程本地的 SecurityContext 包含一个(非匿名的)Authentication 对象,该过滤器会假定用户已由过滤器链中前一个过滤器完成认证。随后,它将调用已配置的 SessionAuthenticationStrategy。
如果用户当前未通过身份验证,过滤器将检查是否请求了无效的会话ID(例如由于超时),并在已配置的情况下调用 InvalidSessionStrategy。最常见的处理方式是直接重定向到固定URL,这一行为已被封装在标准实现 SimpleRedirectInvalidSessionStrategy 中。通过命名空间配置无效会话URL时(如前文所述),同样会使用此策略。
弃用 SessionManagementFilter
在 Spring Security 5 中,默认配置依赖于 SessionManagementFilter 来检测用户是否刚刚完成认证,并调用 SessionAuthenticationStrategy。但这样做的问题是,在典型的配置中,这意味着每个请求都必须读取 HttpSession。
在 Spring Security 6 中,默认情况下认证机制本身必须调用 SessionAuthenticationStrategy。这意味着无需检测 Authentication 何时完成,因此无需为每个请求读取 HttpSession。
迁移 SessionManagementFilter 时的注意事项
在 Spring Security 6 中,默认情况下不再使用 SessionManagementFilter,因此,sessionManagement DSL 中的某些方法将不会产生任何效果。
| 方法 | 替代方案 |
|---|---|
sessionAuthenticationErrorUrl | 在你的认证机制中配置一个 AuthenticationFailureHandler |
sessionAuthenticationFailureHandler | 在你的认证机制中配置一个 AuthenticationFailureHandler |
sessionAuthenticationStrategy | 在你的认证机制中配置一个 SessionAuthenticationStrategy,如上文所述 |
如果你尝试使用这些方法中的任何一个,都会抛出异常。
自定义认证信息存储位置
默认情况下,Spring Security 会将安全上下文存储在 HTTP 会话中。然而,你可能出于以下几种原因想要自定义这一行为:
-
你可能需要在
HttpSessionSecurityContextRepository实例上调用单独的设置器 -
你可能需要将安全上下文存储在缓存或数据库中,以实现水平扩展
首先,你需要创建一个 SecurityContextRepository 的实现,或者使用现有的实现,例如 HttpSessionSecurityContextRepository,然后你可以在 HttpSecurity 中设置它。
- 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" />
上述配置在 SecurityContextHolderFilter 及参与式认证过滤器(如 UsernamePasswordAuthenticationFilter)上设置了 SecurityContextRepository。若要在无状态过滤器中也进行设置,请参阅如何为无状态认证自定义 SecurityContextRepository。
如果你使用的是自定义身份验证机制,可能需要自行存储身份验证信息。
手动存储 Authentication 对象
在某些情况下,例如,您可能需要手动验证用户身份,而不是依赖Spring Security过滤器。您可以使用自定义过滤器或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
}
向控制器添加
SecurityContextRepository注入
HttpServletRequest和HttpServletResponse以便能够保存SecurityContext使用提供的凭据创建一个未经认证的
UsernamePasswordAuthenticationToken调用
AuthenticationManager#authenticate来认证用户创建一个
SecurityContext并在其中设置Authentication将
SecurityContext保存到SecurityContextRepository中
就是这样。如果你不确定上面示例中的 securityContextHolderStrategy 是什么,可以在使用 SecurityContextStrategy 章节中了解更多信息。
正确清除认证
如果你正在使用 Spring Security 的登出支持,那么它会为你处理许多事务,包括清除和保存上下文。但是,假设你需要手动将用户从你的应用中登出。在这种情况下,你需要确保正确清除和保存上下文。
为无状态认证配置持久化
有时,我们并不需要创建和维护一个 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>
上述配置将 SecurityContextRepository 配置 为使用 NullSecurityContextRepository,同时 防止请求被保存到会话中。
在会话中存储无状态认证
如果出于某些原因,您正在使用无状态的身份验证机制,但仍希望将身份验证信息存储在会话中,那么可以使用 HttpSessionSecurityContextRepository 来代替 NullSecurityContextRepository。
对于 HTTP Basic 认证,你可以添加一个 ObjectPostProcessor,用于修改 BasicAuthenticationFilter 所使用的 SecurityContextRepository:
- 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。
理解显式保存要求
在Spring Security 5中,默认行为是通过SecurityContextPersistenceFilter将SecurityContext自动保存到SecurityContextRepository。保存操作必须在HttpServletResponse提交之前、且在SecurityContextPersistenceFilter执行之前完成。遗憾的是,当请求尚未完成时(即在提交HttpServletResponse之前)自动持久化SecurityContext可能会让用户感到意外。同时,跟踪状态以确定是否需要保存的操作较为复杂,有时会导致对SecurityContextRepository(例如HttpSession)进行不必要的写入。
基于这些原因,SecurityContextPersistenceFilter 已被弃用,并由 SecurityContextHolderFilter 替代。在 Spring Security 6 中,默认行为是 SecurityContextHolderFilter 只会从 SecurityContextRepository 读取 SecurityContext 并将其填充到 SecurityContextHolder 中。现在,如果用户希望 SecurityContext 在请求之间保持持久化,则必须使用 SecurityContextRepository 显式地保存 SecurityContext。这种做法消除了歧义,并通过仅在必要时才写入 SecurityContextRepository(例如 HttpSession)来提高性能。
工作原理
总而言之,当 requireExplicitSave 为 true 时,Spring Security 会设置 SecurityContextHolderFilter 而非 SecurityContextPersistenceFilter。
配置并发会话控制
若您希望对单个用户的登录能力施加限制,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>
这将防止用户多次登录——第二次登录将使第一次登录失效。
您也可以根据用户身份进行调整。例如,管理员可能被允许拥有多个会话:
- Java
- Kotlin
- XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
AuthorizationManager<?> isAdmin = AuthorityAuthorizationManager.hasRole("ADMIN");
http
.sessionManagement((session) -> session
.maximumSessions((authentication) -> isAdmin.authorize(() -> authentication, null).isGranted() ? -1 : 1)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val isAdmin: AuthorizationManager<*> = AuthorityAuthorizationManager.hasRole("ADMIN")
http {
sessionManagement {
sessionConcurrency {
maximumSessions {
authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1
}
}
}
}
return http.build()
}
<http>
...
<session-management>
<concurrency-control max-sessions-ref="sessionLimit" />
</session-management>
</http>
<b:bean id="sessionLimit" class="my.SessionLimitImplementation"/>
使用 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-management 元素中添加 session-authentication-error-url 属性。
使用 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());
}
}
如果您正在为基于表单的登录使用自定义身份验证过滤器,那么您必须显式配置并发会话控制支持。您可以通过 Maximum Sessions Prevent Login 示例 来尝试此功能。
如果你正在使用自定义的 UserDetails 实现,请确保重写 equals() 和 hashCode() 方法。Spring Security 中的默认 SessionRegistry 实现依赖于一个内存中的 Map,该 Map 使用这些方法来正确识别和管理用户会话。如果未能重写这些方法,可能会导致会话跟踪和用户比较出现意外行为。
检测超时
会话会自动过期,无需额外操作来确保安全上下文被移除。尽管如此,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并未被清除,即使用户已注销,该Cookie仍会被重新提交。若您遇到此情况,建议配置注销功能以清除会话Cookie。
自定义无效会话策略
invalidSessionUrl 是一个便捷方法,用于通过 SimpleRedirectInvalidSessionStrategy 实现 来设置 InvalidSessionStrategy。如果您想自定义行为,可以实现 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>
注销时清除会话 Cookie
您可以在注销时显式删除JSESSIONID cookie,例如通过在注销处理程序中使用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的mod_headers模块,以下指令通过在注销请求的响应中使JSESSIONID cookie过期来删除它(假设应用程序部署在/tutorial路径下):
<LocationMatch "/tutorial/logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
</LocationMatch>
理解会话固定攻击防护
会话固定攻击是一种潜在风险,恶意攻击者可能通过访问网站创建一个会话,然后诱使另一个用户使用同一会话登录(例如,通过发送包含会话标识符作为参数的链接)。Spring Security 通过在用户登录时创建新会话或更改会话 ID来自动防范此类攻击。
配置会话固定保护
您可以通过以下三种推荐选项来控制会话固定保护策略:
-
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,这种保护还会导致任何 jakarta.servlet.http.HttpSessionIdListener 被通知,因此如果你的代码同时监听这两种事件,请谨慎处理。
你也可以将会话固定保护设置为 none 来禁用它,但这并不推荐,因为这会使得你的应用程序容易受到攻击。
使用 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对象。 -
将
SecurityContext实例静态地设置到SecurityContextHolder中。
虽然上述代码运行良好,但它可能会产生一些不理想的效果:当组件通过 SecurityContextHolder 静态访问 SecurityContext 时,如果存在多个想要指定 SecurityContextHolderStrategy 的应用上下文,就可能产生竞态条件。这是因为在 SecurityContextHolder 中,每个类加载器只有一个策略,而不是每个应用上下文一个。
为了解决这个问题,组件可以从应用上下文中装配 SecurityContextHolderStrategy。默认情况下,它们仍会从 SecurityContextHolder 中查找策略。
这些改动主要是内部的,但它们为应用程序提供了通过自动装配 SecurityContextHolderStrategy 来替代静态访问 SecurityContext 的机会。为此,您应将代码修改如下:
- 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实例。
强制立即创建会话
在某些情况下,提前创建会话可能很有价值。这可以通过使用 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>
后续阅读建议
- 使用 Spring Session 实现集群会话
1. 通过认证后执行重定向的机制(例如表单登录)进行的认证将不会被 SessionManagementFilter 检测到,因为在认证请求期间不会调用该过滤器。在这些情况下,会话管理功能必须单独处理。