对于最新的稳定版本,请使用 Spring Security 6.5.3! |
身份验证持久性和会话管理
一旦您拥有了一个正在验证请求的应用程序,请务必考虑如何在将来的请求中保留和恢复生成的身份验证。
默认情况下,这是自动完成的,因此不需要额外的代码,但重要的是要知道什么requireExplicitSave
意味着HttpSecurity
.
如果你愿意,可以阅读更多关于 requireExplicitSave 正在做什么或为什么它很重要的信息。否则,在大多数情况下,您已完成此部分。
但在离开之前,请考虑以下任何用例是否适合您的应用程序:
-
我想限制用户可以同时登录的次数
-
我想自己直接存储身份验证,而不是 Spring Security 为我做
-
我正在手动存储身份验证,我想将其删除
-
我正在使用
SessionManagementFilter
我需要指导来摆脱这一点 -
我想将身份验证存储在会话以外的其他内容中
-
我正在使用无状态身份验证,但我仍然想将其存储在会话中
-
我正在使用
SessionCreationPolicy.NEVER
但应用程序仍在创建会话。
了解会话管理的组件
会话管理支持由几个组件组成,这些组件协同工作以提供功能。
这些组件是,这SecurityContextHolderFilter
,这SecurityContextPersistenceFilter
和这SessionManagementFilter
.
在 Spring Security 6 中, |
这SessionManagementFilter
这SessionManagementFilter
检查SecurityContextRepository
针对SecurityContextHolder
确定用户是否在当前请求期间进行了身份验证,通常通过非交互式身份验证机制(例如预身份验证或记住我)[1].
如果存储库包含安全上下文,则过滤器不执行任何作。
如果没有,则 thread-localSecurityContext
包含一个(非匿名)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 不会有任何影响。
方法 | 更换 |
---|---|
|
配置一个 |
|
配置一个 |
|
配置一个 |
如果您尝试使用这些方法中的任何一种,则会抛出异常。
自定义身份验证的存储位置
默认情况下,Spring Security 将安全上下文存储在 HTTP 会话中。但是,您可能想要自定义它的几个原因:
-
您可能希望在
HttpSessionSecurityContextRepository
实例 -
您可能希望将安全上下文存储在缓存或数据库中以启用水平缩放
首先,您需要创建一个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" />
上述配置将 |
如果您使用的是自定义身份验证机制,则可能需要存储Authentication
自己.
存储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
}
1 | 添加SecurityContextRepository 到控制器 |
2 | 注入HttpServletRequest 和HttpServletResponse 以便能够保存SecurityContext |
3 | 创建未经身份验证的UsernamePasswordAuthenticationToken 使用提供的凭据 |
4 | 叫AuthenticationManager#authenticate 对用户进行身份验证 |
5 | 创建一个SecurityContext 并将Authentication 在里面 |
6 | 保存SecurityContext 在SecurityContextRepository |
就是这样。如果您不确定是什么securityContextHolderStrategy
在上面的例子中,你可以在用SecurityContextStrategy
部分.
正确清除身份验证
如果您使用的是 Spring Security 的注销支持,那么它会为您处理很多事情,包括清除和保存上下文。但是,假设您需要手动将用户注销出您的应用程序。在这种情况下,您需要确保正确清除和保存上下文。
现在,您可能已经熟悉清除SecurityContextHolder
通过做SecurityContextHolderStrategy#clearContext()
. 这很好,但如果您的应用程序需要显式保存上下文,则仅清除它是不够的。原因是它不会将其从SecurityContextRepository
,这意味着SecurityContext
仍然可以满足下一个请求,我们绝对不希望这样。
为确保正确清除和保存身份验证,可以调用这SecurityContextLogoutHandler
它为我们做到了这一点,如下所示:
-
Java
SecurityContextLogoutHandler handler = new SecurityContextLogoutHandler(); (1)
handler.logout(httpServletRequest, httpServletResponse, null); (2)
1 | 创建SecurityContextLogoutHandler |
2 | 调用logout 方法传递给HttpServletRequest ,HttpServletResponse 和null 身份验证,因为此处理程序不需要它。 |
重要的是要记住,清除和保存上下文只是注销过程的一部分,因此我们建议让 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
并且还阻止将请求保存在会话中。
如果您正在使用SessionCreationPolicy.NEVER
,您可能会注意到应用程序仍在创建HttpSession
.
在大多数情况下,发生这种情况是因为请求保存在会话中,以便经过身份验证的资源在身份验证成功后重新请求。
为避免这种情况,请参阅如何阻止请求被保存部分。
在会话中存储无状态身份验证
如果由于某种原因,您正在使用无状态身份验证机制,但您仍然希望将身份验证存储在会话中,则可以使用HttpSessionSecurityContextRepository
而不是NullSecurityContextRepository
.
对于 HTTP Basic,您可以添加一个ObjectPostProcessor
这会改变SecurityContextRepository
由BasicAuthenticationFilter
:
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();
}
上述内容也适用于其他身份验证机制,例如持有者Tokens身份验证。
了解需要显式保存
在 Spring Security 5 中,默认行为是SecurityContext
自动保存到SecurityContextRepository
使用SecurityContextPersistenceFilter
.
保存必须在HttpServletResponse
正在承诺和就在之前SecurityContextPersistenceFilter
.
不幸的是,自动持久化SecurityContext
在请求完成之前(即在提交HttpServletResponse
).
跟踪状态以确定是否需要保存也很复杂,这会导致对SecurityContextRepository
(即HttpSession
)有时。
由于这些原因,SecurityContextPersistenceFilter
已被弃用,替换为SecurityContextHolderFilter
.
在 Spring Security 6 中,默认行为是这SecurityContextHolderFilter
只会读取SecurityContext
从SecurityContextRepository
并将其填充在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>
这将防止用户多次登录 - 第二次登录将导致第一次登录失效。
使用 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());
}
}
如果使用自定义身份验证过滤器进行基于表单的登录,则必须显式配置并发会话控制支持。 您可以使用最大会话阻止登录示例进行尝试。
检测超时
会话会自行过期,无需执行任何作即可确保删除安全上下文。
也就是说,Spring Security 可以检测会话何时过期并采取您指示的特定作。
例如,当用户使用已过期的会话发出请求时,您可能希望重定向到特定端点。
这是通过invalidSessionUrl
在HttpSecurity
:
-
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。
自定义无效会话策略
这invalidSessionUrl
是一种方便的方法,用于设置InvalidSessionStrategy
使用SimpleRedirectInvalidSessionStrategy
实现.
如果要自定义行为,可以实现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 的 |
<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
s 时收到通知,因此如果您的代码侦听这两个事件,请小心。
您还可以将会话固定保护设置为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)
-
创建一个空的
SecurityContext
实例,通过访问SecurityContextHolder
静态。 -
将
Authentication
对象SecurityContext
实例。 -
将
SecurityContext
实例中的SecurityContextHolder
静态。
虽然上面的代码工作正常,但它可能会产生一些不良效果:当组件访问SecurityContext
静态通过SecurityContextHolder
,当有多个应用程序上下文想要指定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)
}
}
-
创建一个空的
SecurityContext
实例使用配置的SecurityContextHolderStrategy
. -
将
Authentication
对象SecurityContext
实例。 -
将
SecurityContext
实例中的SecurityContextHolderStrategy
.
强制创建预切会话
有时,急切地创建会话可能很有价值。这可以通过使用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 会话的集群会话
SessionManagementFilter
,因为在身份验证请求期间不会调用过滤器。在这些情况下,会话管理功能必须单独处理。