并发会话控制

Servlet 的并发会话控制 类似,Spring Security 也提供了在响应式应用中限制用户并发会话数量的支持。

在 Spring Security 中设置并发会话控制时,它会通过介入表单登录 (OAuth 2.0 登录) 和 HTTP Basic 认证处理认证成功的方式来监控这些认证。更具体地说,会话管理 DSL 会将 ConcurrentSessionControlServerAuthenticationSuccessHandlerRegisterSessionServerAuthenticationSuccessHandler 添加到认证过滤器使用的 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>>,因此您可以实现更复杂的逻辑,根据用户的认证来确定最大会话数量

根据 Authentication 配置 maximumSessions
  • 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 实现。

配置 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()
}

指定 ReactiveSessionRegistry

为了跟踪用户的会话,Spring Security 使用 ReactiveSessionRegistry,并且每当用户登录时,其会话信息都会被保存。

Spring Security 提供了 ReactiveSessionRegistry 的实现 InMemoryReactiveSessionRegistry

要指定 ReactiveSessionRegistry 实现,您可以将其声明为一个 Bean

将 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 方法

使用 sessionRegistry DSL 方法的 ReactiveSessionRegistry
  • 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 中移除

使用 ReactiveSessionRegistry 手动使会话失效
  • 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();
}

查看示例应用

您可以在此处查看示例应用