同時セッションの制御

サーブレットの同時セッション制御と同様に、Spring Security も、ユーザーが Reactive アプリケーションで保持できる同時セッションの数を制限するサポートを提供します。

Spring Security で同時セッション制御を設定すると、フォームログイン、OAuth 2.0 ログイン、HTTP 基本認証を通じて実行される認証を、これらの認証メカニズムが認証の成功を処理する方法にフックすることによって監視します。具体的には、セッション管理 DSL は、認証フィルターで使用される ServerAuthenticationSuccessHandler のリストに ConcurrentSessionControlServerAuthenticationSuccessHandler (Javadoc) RegisterSessionServerAuthenticationSuccessHandler (Javadoc) を追加します。

次のセクションには、同時セッション制御を構成する方法の例が含まれています。

同時セッションの制限

デフォルトでは、Spring Security はユーザーに対して任意の数の同時セッションを許可します。同時セッションの数を制限するには、maximumSessions DSL メソッドを使用できます。

任意のユーザーに対して 1 つのセッションを構成する
  • 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()
}

上記の構成では、どのユーザーに対しても 1 つのセッションが許可されます。同様に、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 実装を含める必要があります。

最大セッション数を超えた場合の処理

デフォルトでは、セッションの最大数を超えると、最も最近使用されていないセッションが InvalidateLeastUsedMaximumSessionsExceededHandler (Javadoc) を使用して期限切れになります。Spring Security は、ユーザーが PreventLoginMaximumSessionsExceededHandler (Javadoc) を使用して新しいセッションを作成できないようにする別の実装も提供します。独自の戦略を使用したい場合は、ServerMaximumSessionsExceededHandler (Javadoc) の別の実装を提供できます。

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 (Javadoc) を使用し、ユーザーがログインするたびにセッション情報が保存されます。

Spring Security には、ReactiveSessionRegistryInMemoryReactiveSessionRegistry (Javadoc) 実装が付属しています。

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 基本認証では、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();
}

サンプルアプリケーションの確認