并发会话控制
类似于 Servlet 的并发会话控制,Spring Security 同样支持在响应式应用中限制单个用户的并发会话数量。
当你在Spring Security中设置并发会话控制时,它会通过挂钩到这些认证机制处理认证成功的方式,来监控通过表单登录、OAuth 2.0登录和HTTP基本认证执行的认证。更具体地说,会话管理DSL会将ConcurrentSessionControlServerAuthenticationSuccessHandler和RegisterSessionServerAuthenticationSuccessHandler添加到认证过滤器所使用的ServerAuthenticationSuccessHandler列表中。
以下部分包含如何配置并发会话控制的示例。
限制并发会话
默认情况下,Spring Security 允许用户拥有任意数量的并发会话。要限制并发会话的数量,你可以使用 maximumSessions DSL 方法:
- Java
- Kotlin
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(SessionLimit.of(1))
)
);
return http.build();
}
@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
return new InMemoryReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(1)
}
}
}
}
@Bean
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
return InMemoryReactiveSessionRegistry()
}
上述配置允许任意用户拥有一个会话。同样,你也可以通过使用 SessionLimit#UNLIMITED 常量来允许无限数量的会话:
- Java
- Kotlin
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(SessionLimit.UNLIMITED))
);
return http.build();
}
@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
return new InMemoryReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.UNLIMITED
}
}
}
}
@Bean
open fun reactiveSessionRegistry(webSessionManager: WebSessionManager): ReactiveSessionRegistry {
return InMemoryReactiveSessionRegistry()
}
由于 maximumSessions 方法接受一个 SessionLimit 接口,而该接口又扩展了 Function<Authentication, Mono<Integer>>,因此你可以基于用户的认证信息实现更复杂的逻辑来确定最大会话数:
- Java
- Kotlin
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(maxSessions()))
);
return http.build();
}
private SessionLimit maxSessions() {
return (authentication) -> {
if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_UNLIMITED_SESSIONS"))) {
return Mono.empty(); // allow unlimited sessions for users with ROLE_UNLIMITED_SESSIONS
}
if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
return Mono.just(2); // allow two sessions for admins
}
return Mono.just(1); // allow one session for every other user
};
}
@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
return new InMemoryReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
sessionManagement {
sessionConcurrency {
maximumSessions = maxSessions()
}
}
}
}
fun maxSessions(): SessionLimit {
return { authentication ->
if (authentication.authorities.contains(SimpleGrantedAuthority("ROLE_UNLIMITED_SESSIONS"))) Mono.empty
if (authentication.authorities.contains(SimpleGrantedAuthority("ROLE_ADMIN"))) Mono.just(2)
Mono.just(1)
}
}
@Bean
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
return InMemoryReactiveSessionRegistry()
}
当会话数量超过最大限制时,默认情况下,最近最少使用的会话将被过期处理。如果您希望更改此行为,可以自定义超出最大会话数时的处理策略。
并发会话管理无法感知到你可能通过 OAuth 2 登录 等方式使用的其他身份提供者中是否存在其他会话。如果你还需要使身份提供者处的会话失效,则必须包含你自己的 ServerMaximumSessionsExceededHandler 实现。
处理最大会话数超限
默认情况下,当超过最大会话数时,系统将使用 InvalidateLeastUsedServerMaximumSessionsExceededHandler 来使最近最少使用的会话失效。Spring Security 还提供了另一种实现,即通过 PreventLoginServerMaximumSessionsExceededHandler 来阻止用户创建新会话。如果您希望使用自己的策略,可以提供不同的 ServerMaximumSessionsExceededHandler 实现。
- Java
- Kotlin
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(SessionLimit.of(1))
.maximumSessionsExceededHandler(new PreventLoginMaximumSessionsExceededHandler())
)
);
return http.build();
}
@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
return new InMemoryReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(1)
maximumSessionsExceededHandler = PreventLoginMaximumSessionsExceededHandler()
}
}
}
}
@Bean
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
return InMemoryReactiveSessionRegistry()
}
指定 ReactiveSessionRegistry
为了跟踪用户的会话,Spring Security 使用了 ReactiveSessionRegistry,并且每当用户登录时,他们的会话信息都会被保存。
Spring Security 内置了 ReactiveSessionRegistry 的实现 InMemoryReactiveSessionRegistry。
要指定一个 ReactiveSessionRegistry 实现,你可以将其声明为一个 bean:
- Java
- Kotlin
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(SessionLimit.of(1))
)
);
return http.build();
}
@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
return new MyReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(1)
}
}
}
}
@Bean
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
return MyReactiveSessionRegistry()
}
或者你也可以使用 sessionRegistry DSL 方法:
- Java
- Kotlin
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(SessionLimit.of(1))
.sessionRegistry(new MyReactiveSessionRegistry())
)
);
return http.build();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(1)
sessionRegistry = MyReactiveSessionRegistry()
}
}
}
}
使已注册用户的会话失效
有时,能够使用户的全部或部分会话失效会非常方便。例如,当用户更改密码时,您可能希望使其所有会话失效,从而迫使他们重新登录。为此,您可以使用 ReactiveSessionRegistry bean 来检索用户的所有会话,使其失效,然后将它们从 WebSessionStore 中移除:
- Java
public class SessionControl {
private final ReactiveSessionRegistry reactiveSessionRegistry;
private final WebSessionStore webSessionStore;
public Mono<Void> invalidateSessions(String username) {
return this.reactiveSessionRegistry.getAllSessions(username)
.flatMap((session) -> session.invalidate().thenReturn(session))
.flatMap((session) -> this.webSessionStore.removeSession(session.getSessionId()))
.then();
}
}
为部分认证过滤器禁用
默认情况下,只要表单登录、OAuth 2.0 登录和 HTTP Basic 认证没有自行指定 ServerAuthenticationSuccessHandler,就会为它们自动配置并发会话控制。例如,以下配置将禁用表单登录的并发会话控制:
- Java
- Kotlin
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin((login) -> login
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/"))
)
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(SessionLimit.of(1))
)
);
return http.build();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
formLogin {
authenticationSuccessHandler = RedirectServerAuthenticationSuccessHandler("/")
}
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(1)
}
}
}
}
添加额外的成功处理器而不禁用并发会话控制
您还可以在不禁用并发会话控制的情况下,向认证过滤器使用的处理器列表中添加额外的 ServerAuthenticationSuccessHandler 实例。为此,您可以使用 authenticationSuccessHandler(Consumer<List<ServerAuthenticationSuccessHandler>>) 方法:
- Java
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
// ...
.formLogin((login) -> login
.authenticationSuccessHandler((handlers) -> handlers.add(new MyAuthenticationSuccessHandler()))
)
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(SessionLimit.of(1))
)
);
return http.build();
}
检查示例应用程序
你可以在此查看示例应用。