Concurrent Sessions Control

Servlet’s Concurrent Sessions Control类似,Spring Security 还提供支持,以限制用户在响应式应用程序中可同时进行的会话数。 在 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`列表中。 以下部分包含一些配置并发会话控制的示例。

Limiting Concurrent Sessions

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

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 常量允许无限会话:

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

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

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] 的不同实现。

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],并且每次用户登录时,他们的会话信息都会被保存。

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

要指定 ReactiveSessionRegistry 实现,可以将其声明为 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 方法:

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 中删除它们:

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 会话控制:

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>>) 方法:

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

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