Concurrent Sessions Control

Servlet’s Concurrent Sessions Control类似,Spring Security 还提供支持,以限制用户在响应式应用程序中可同时进行的会话数。

Similar to Servlet’s Concurrent Sessions Control, Spring Security also provides support to limit the number of concurrent sessions a user can have in a Reactive application.

在 Spring Security 中设置并发会话控制时,它通过挂接到这些身份验证机制处理身份验证成功的方式,来监视通过 Form 登录、OAuth 2.0 Login和 HTTP Basic 身份验证执行的身份验证。更具体地说,会话管理 DSL 会将 {security-api-url}org/springframework/security/web/server/authentication/ConcurrentSessionControlServerAuthenticationSuccessHandler.html[ConcurrentSessionControlServerAuthenticationSuccessHandler] 和 {security-api-url}org/springframework/security/web/server/authentication/RegisterSessionServerAuthenticationSuccessHandler.html[RegisterSessionServerAuthenticationSuccessHandler] 添加到身份验证筛选器使用的 `ServerAuthenticationSuccessHandler`列表中。

When you set up Concurrent Sessions Control in Spring Security, it monitors authentications carried out through Form Login, OAuth 2.0 Login, and HTTP Basic authentication by hooking into the way those authentication mechanisms handle authentication success. More specifically, the session management DSL will add the {security-api-url}org/springframework/security/web/server/authentication/ConcurrentSessionControlServerAuthenticationSuccessHandler.html[ConcurrentSessionControlServerAuthenticationSuccessHandler] and the {security-api-url}org/springframework/security/web/server/authentication/RegisterSessionServerAuthenticationSuccessHandler.html[RegisterSessionServerAuthenticationSuccessHandler] to the list of ServerAuthenticationSuccessHandler used by the authentication filter.

以下部分包含一些配置并发会话控制的示例。

The following sections contains examples of how to configure Concurrent Sessions Control.

Limiting Concurrent Sessions

默认情况下,Spring Security 将允许为用户无限数量的并发会话。为限制并发会话数,你可以使用 maximumSessions DSL 方法:

By default, Spring Security will allow any number of concurrent sessions for a user. To limit the number of concurrent sessions, you can use the maximumSessions DSL method:

Configuring one session for any user
  • 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 常量允许无限会话:

The above configuration allows one session for any user. Similarly, you can also allow unlimited sessions by using the SessionLimit#UNLIMITED constant:

Configuring unlimited sessions
  • 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>>),你便可以按照更复杂的逻辑根据用户的认证确定最大会话数:

Since the maximumSessions method accepts a SessionLimit interface, which in turn extends Function<Authentication, Mono<Integer>>, you can have a more complex logic to determine the maximum number of sessions based on the user’s authentication:

Configuring maximumSessions based on Authentication
  • 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()
}

超出最大会话数时,默认情况下,最不常用的会话将过期。如果你想更改这种行为,可以customize the strategy used when the maximum number of sessions is exceeded

When the maximum number of sessions is exceeded, by default, the least recently used session(s) will be expired. If you want to change that behavior, you can concurrent-sessions-control-custom-strategy.

Handling Maximum Number of Sessions Exceeded

默认情况下,当超出最大会话数时,最少最近使用的会话将使用 {security-api-url}org/springframework/security/web/server/authentication/session/InvalidateLeastUsedMaximumSessionsExceededHandler.html[InvalidateLeastUsedMaximumSessionsExceededHandler] 过期。Spring Security 还提供了防止用户使用 {security-api-url}org/springframework/security/web/server/authentication/session/PreventLoginMaximumSessionsExceededHandler.html[PreventLoginMaximumSessionsExceededHandler] 创建新会话的另一实现。如果你想使用自己的策略,则可以提供 {security-api-url}org/springframework/security/web/server/authentication/session/ServerMaximumSessionsExceededHandler.html[ServerMaximumSessionsExceededHandler] 的不同实现。

By default, when the maximum number of sessions is exceeded, the least recently used session(s) will be expired by using the {security-api-url}org/springframework/security/web/server/authentication/session/InvalidateLeastUsedMaximumSessionsExceededHandler.html[InvalidateLeastUsedMaximumSessionsExceededHandler]. Spring Security also provides another implementation that prevents the user from creating new sessions by using the {security-api-url}org/springframework/security/web/server/authentication/session/PreventLoginMaximumSessionsExceededHandler.html[PreventLoginMaximumSessionsExceededHandler]. If you want to use your own strategy, you can provide a different implementation of {security-api-url}org/springframework/security/web/server/authentication/session/ServerMaximumSessionsExceededHandler.html[ServerMaximumSessionsExceededHandler].

Configuring maximumSessionsExceededHandler
  • 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()
}

Specifying a ReactiveSessionRegistry

为了跟踪用户的会话,Spring Security 使用了 {security-api-url}org/springframework/security/core/session/ReactiveSessionRegistry.html[ReactiveSessionRegistry],并且每次用户登录时,他们的会话信息都会被保存。

In order to keep track of the user’s sessions, Spring Security uses a {security-api-url}org/springframework/security/core/session/ReactiveSessionRegistry.html[ReactiveSessionRegistry], and, every time a user logs in, their session information is saved.

Spring Security 内置了 {security-api-url}org/springframework/security/core/session/InMemoryReactiveSessionRegistry.html[InMemoryReactiveSessionRegistry],用于实现 ReactiveSessionRegistry

Spring Security ships with {security-api-url}org/springframework/security/core/session/InMemoryReactiveSessionRegistry.html[InMemoryReactiveSessionRegistry] implementation of ReactiveSessionRegistry.

要指定 ReactiveSessionRegistry 实现,可以将其声明为 Bean:

To specify a ReactiveSessionRegistry implementation you can either declare it as a bean:

ReactiveSessionRegistry as a 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 方法:

or you can use the sessionRegistry DSL method:

ReactiveSessionRegistry using sessionRegistry DSL method
  • 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()
            }
        }
    }
}

Invalidating Registered User’s Sessions

有时,能够使所有或部分用户会话失效非常方便。例如,当用户更改密码时,你可能希望使所有会话失效,以便迫使他们重新登录。要做到这一点,可以使用 ReactiveSessionRegistry Bean 检索所有用户会话,使它们失效,然后从 WebSessionStore 中删除它们:

At times, it is handy to be able to invalidate all or some of a user’s sessions. For example, when a user changes their password, you may want to invalidate all of their sessions so that they are forced to log in again. To do that, you can use the ReactiveSessionRegistry bean to retrieve all the user’s sessions, invalidate them, and them remove them from the WebSessionStore:

Using ReactiveSessionRegistry to invalidate sessions manually
  • 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();
    }
}

Disabling It for Some Authentication Filters

默认情况下,只要 Concurrent 会话控制不由 ServerAuthenticationSuccessHandler 本身指定,它将针对表单登录、OAuth 2.0 登录和 HTTP 基本身份验证自动配置。例如,以下配置将禁用表单登录的 Concurrent 会话控制:

By default, Concurrent Sessions Control will be configured automatically for Form Login, OAuth 2.0 Login, and HTTP Basic authentication as long as they do not specify an ServerAuthenticationSuccessHandler themselves. For example, the following configuration will disable Concurrent Sessions Control for Form Login:

Disabling Concurrent Sessions Control for Form Login
  • 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)
            }
        }
    }
}

Adding Additional Success Handlers Without Disabling Concurrent Sessions Control

也可以在身份验证过滤器使用的处理程序列表中包含其他 ServerAuthenticationSuccessHandler 实例,而无需禁用 Concurrent 会话控制。要做到这一点,可以使用 authenticationSuccessHandler(Consumer<List<ServerAuthenticationSuccessHandler>>) 方法:

You can also include additional ServerAuthenticationSuccessHandler instances to the list of handlers used by the authentication filter without disabling Concurrent Sessions Control. To do that you can use the authenticationSuccessHandler(Consumer<List<ServerAuthenticationSuccessHandler>>) method:

Adding additional handlers
  • 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();
}

Checking a Sample Application

你可以点击 此处 查看示例应用程序。

You can check the sample application here.