You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
464 lines
14 KiB
464 lines
14 KiB
[[reactive-concurrent-sessions-control]] |
|
= Concurrent Sessions Control |
|
|
|
Similar to xref:servlet/authentication/session-management.adoc#ns-concurrent-sessions[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. |
|
|
|
When you set up Concurrent Sessions Control in Spring Security, it monitors authentications carried out through Form Login, xref:reactive/oauth2/login/index.adoc[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 javadoc:org.springframework.security.web.server.authentication.ConcurrentSessionControlServerAuthenticationSuccessHandler[] and the javadoc:org.springframework.security.web.server.authentication.RegisterSessionServerAuthenticationSuccessHandler[] to the list of `ServerAuthenticationSuccessHandler` used by the authentication filter. |
|
|
|
The following sections contains examples of how to configure Concurrent Sessions Control. |
|
|
|
* <<reactive-concurrent-sessions-control-limit,I want to limit the number of concurrent sessions a user can have>> |
|
* <<concurrent-sessions-control-custom-strategy,I want to customize the strategy used when the maximum number of sessions is exceeded>> |
|
* <<reactive-concurrent-sessions-control-specify-session-registry,I want to know how to specify a `ReactiveSessionRegistry`>> |
|
* <<concurrent-sessions-control-sample,I want to see a sample application that uses Concurrent Sessions Control>> |
|
* <<disabling-for-authentication-filters,I want to know how to disable it for some authentication filter>> |
|
|
|
[[reactive-concurrent-sessions-control-limit]] |
|
== Limiting Concurrent Sessions |
|
|
|
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 |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { |
|
return http { |
|
// ... |
|
sessionManagement { |
|
sessionConcurrency { |
|
maximumSessions = SessionLimit.of(1) |
|
} |
|
} |
|
} |
|
} |
|
@Bean |
|
open fun reactiveSessionRegistry(): ReactiveSessionRegistry { |
|
return InMemoryReactiveSessionRegistry() |
|
} |
|
---- |
|
====== |
|
|
|
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 |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
SecurityWebFilterChain filterChain(ServerHttpSecurity http) { |
|
http |
|
// ... |
|
.sessionManagement((sessions) -> sessions |
|
.concurrentSessions((concurrency) -> concurrency |
|
.maximumSessions(SessionLimit.UNLIMITED)) |
|
); |
|
return http.build(); |
|
} |
|
|
|
@Bean |
|
ReactiveSessionRegistry reactiveSessionRegistry() { |
|
return new InMemoryReactiveSessionRegistry(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { |
|
return http { |
|
// ... |
|
sessionManagement { |
|
sessionConcurrency { |
|
maximumSessions = SessionLimit.UNLIMITED |
|
} |
|
} |
|
} |
|
} |
|
@Bean |
|
open fun reactiveSessionRegistry(webSessionManager: WebSessionManager): ReactiveSessionRegistry { |
|
return InMemoryReactiveSessionRegistry() |
|
} |
|
---- |
|
====== |
|
|
|
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` |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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() |
|
} |
|
---- |
|
====== |
|
|
|
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,customize the strategy used when the maximum number of sessions is exceeded>>. |
|
|
|
[IMPORTANT] |
|
==== |
|
The Concurrent Session Management is not aware if there is another session in some Identity Provider that you might use via xref:reactive/oauth2/login/index.adoc[OAuth 2 Login] for example. |
|
If you also need to invalidate the session against the Identity Provider you must <<concurrent-sessions-control-custom-strategy,include your own implementation of `ServerMaximumSessionsExceededHandler`>>. |
|
==== |
|
|
|
[[concurrent-sessions-control-custom-strategy]] |
|
== Handling Maximum Number of Sessions Exceeded |
|
|
|
By default, when the maximum number of sessions is exceeded, the least recently used session(s) will be expired by using the javadoc:org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler[]. |
|
Spring Security also provides another implementation that prevents the user from creating new sessions by using the javadoc:org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler[]. |
|
If you want to use your own strategy, you can provide a different implementation of javadoc:org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler[]. |
|
|
|
.Configuring maximumSessionsExceededHandler |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { |
|
return http { |
|
// ... |
|
sessionManagement { |
|
sessionConcurrency { |
|
maximumSessions = SessionLimit.of(1) |
|
maximumSessionsExceededHandler = PreventLoginMaximumSessionsExceededHandler() |
|
} |
|
} |
|
} |
|
} |
|
|
|
@Bean |
|
open fun reactiveSessionRegistry(): ReactiveSessionRegistry { |
|
return InMemoryReactiveSessionRegistry() |
|
} |
|
---- |
|
====== |
|
|
|
[[reactive-concurrent-sessions-control-specify-session-registry]] |
|
== Specifying a `ReactiveSessionRegistry` |
|
|
|
In order to keep track of the user's sessions, Spring Security uses a javadoc:org.springframework.security.core.session.ReactiveSessionRegistry[], and, every time a user logs in, their session information is saved. |
|
|
|
Spring Security ships with javadoc:org.springframework.security.core.session.InMemoryReactiveSessionRegistry[] implementation of `ReactiveSessionRegistry`. |
|
|
|
To specify a `ReactiveSessionRegistry` implementation you can either declare it as a bean: |
|
|
|
.ReactiveSessionRegistry as a Bean |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { |
|
return http { |
|
// ... |
|
sessionManagement { |
|
sessionConcurrency { |
|
maximumSessions = SessionLimit.of(1) |
|
} |
|
} |
|
} |
|
} |
|
|
|
@Bean |
|
open fun reactiveSessionRegistry(): ReactiveSessionRegistry { |
|
return MyReactiveSessionRegistry() |
|
} |
|
---- |
|
====== |
|
|
|
or you can use the `sessionRegistry` DSL method: |
|
|
|
.ReactiveSessionRegistry using sessionRegistry DSL method |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
SecurityWebFilterChain filterChain(ServerHttpSecurity http) { |
|
http |
|
// ... |
|
.sessionManagement((sessions) -> sessions |
|
.concurrentSessions((concurrency) -> concurrency |
|
.maximumSessions(SessionLimit.of(1)) |
|
.sessionRegistry(new MyReactiveSessionRegistry()) |
|
) |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { |
|
return http { |
|
// ... |
|
sessionManagement { |
|
sessionConcurrency { |
|
maximumSessions = SessionLimit.of(1) |
|
sessionRegistry = MyReactiveSessionRegistry() |
|
} |
|
} |
|
} |
|
} |
|
---- |
|
====== |
|
|
|
[[reactive-concurrent-sessions-control-manually-invalidating-sessions]] |
|
== Invalidating Registered User's Sessions |
|
|
|
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 |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
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-for-authentication-filters]] |
|
== Disabling It for Some Authentication Filters |
|
|
|
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 |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 |
|
|
|
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 |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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(); |
|
} |
|
---- |
|
====== |
|
|
|
[[concurrent-sessions-control-sample]] |
|
== Checking a Sample Application |
|
|
|
You can check the {gh-samples-url}/reactive/webflux/java/session-management/maximum-sessions[sample application here].
|
|
|