此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

并发会话控制

类似于 Servlet 的并发会话控制,Spring Security 也为响应式应用提供了限制用户可以同时拥有的并发会话数的支持。spring-doc.cadn.net.cn

当您在 Spring Security 中配置并发会话控制时,它会通过挂钩这些认证机制处理认证成功的方式,来监控通过表单登录、OAuth 2.0 登录和 HTTP Basic 认证进行的认证。 更具体地说,会话管理 DSL 会将 ConcurrentSessionControlServerAuthenticationSuccessHandlerRegisterSessionServerAuthenticationSuccessHandler 添加到认证过滤器使用的 ServerAuthenticationSuccessHandler 列表中。spring-doc.cadn.net.cn

以下部分包含如何配置并发会话控制的示例。spring-doc.cadn.net.cn

限制并发会话

默认情况下,Spring Security 允许用户有任意数量的并发会话。 要限制并发会话的数量,您可以使用 maximumSessions DSL 方法:spring-doc.cadn.net.cn

配置每个用户的单一会话
@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常量来允许多个会话:spring-doc.cadn.net.cn

配置无限会话
@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>>,因此您可以根据用户的认证信息实现更为复杂的逻辑来确定最大会话数:spring-doc.cadn.net.cn

根据Authentication配置maximumSessions
@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()
}

当会话数量超出最大限制时,默认情况下,最近最少使用的会话将被过期。 如果您想要更改此行为,可以自定义在会话数量超出最大限制时使用的策略spring-doc.cadn.net.cn

并发会话管理无法感知您是否通过OAuth 2 登录等方式使用了某个身份提供商中的其他会话。 如果您还需要使该身份提供商的会话失效,则必须包含您自己对ServerMaximumSessionsExceededHandler的实现spring-doc.cadn.net.cn

已达到最大会话数限制

默认情况下,当会话数量超过最大值时,最近最少使用的会话将使用 InvalidateLeastUsedServerMaximumSessionsExceededHandler 过期。 Spring Security 还提供了另一种实现方式,通过 PreventLoginServerMaximumSessionsExceededHandler 阻止用户创建新会话。 如果您想使用自己的策略,可以提供 ServerMaximumSessionsExceededHandler 的不同实现。spring-doc.cadn.net.cn

配置maximumSessionsExceededHandler
@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-doc.cadn.net.cn

Spring Security 附带了 InMemoryReactiveSessionRegistry 实现,用于 ReactiveSessionRegistryspring-doc.cadn.net.cn

要指定一个ReactiveSessionRegistry实现,您可以声明它为一个bean:spring-doc.cadn.net.cn

ReactiveSessionRegistry 作为 Bean
@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()
}

or 你可以使用 sessionRegistry DSL 方法:spring-doc.cadn.net.cn

使用 sessionRegistry DSL 方法的 ReactiveSessionRegistry
@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中移除它们:spring-doc.cadn.net.cn

使用ReactiveSessionRegistry手动失效会话
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 基本认证,前提是它们没有自己指定 ServerAuthenticationSuccessHandler。 例如,以下配置将禁用表单登录的会话控制:spring-doc.cadn.net.cn

禁用表单登录的并发会话控制
@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>>)方法:spring-doc.cadn.net.cn

添加额外的处理器
@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();
}

检查示例应用程序

您可以在这里查看示例应用spring-doc.cadn.net.cn