Update Max Sessions on WebFlux

Delete WebSessionStoreReactiveSessionRegistry.java and gives the responsibility to remove the sessions from the WebSessionStore to the handler

Issue gh-6192
This commit is contained in:
Marcus Hert Da Coregio 2024-02-28 10:06:45 -03:00
parent f3bcf7ed5d
commit f8ff056eb6
10 changed files with 331 additions and 580 deletions

View File

@ -214,6 +214,8 @@ import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.util.pattern.PathPatternParser;
/**
@ -1964,7 +1966,7 @@ public class ServerHttpSecurity {
private SessionLimit sessionLimit = SessionLimit.UNLIMITED;
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler();
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler;
/**
* Configures how many sessions are allowed for a given user.
@ -1983,9 +1985,8 @@ public class ServerHttpSecurity {
if (this.concurrentSessions != null) {
ReactiveSessionRegistry reactiveSessionRegistry = getSessionRegistry();
ConcurrentSessionControlServerAuthenticationSuccessHandler concurrentSessionControlStrategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(
reactiveSessionRegistry);
reactiveSessionRegistry, getMaximumSessionsExceededHandler());
concurrentSessionControlStrategy.setSessionLimit(this.sessionLimit);
concurrentSessionControlStrategy.setMaximumSessionsExceededHandler(this.maximumSessionsExceededHandler);
RegisterSessionServerAuthenticationSuccessHandler registerSessionAuthenticationStrategy = new RegisterSessionServerAuthenticationSuccessHandler(
reactiveSessionRegistry);
this.authenticationSuccessHandler = new DelegatingServerAuthenticationSuccessHandler(
@ -1997,6 +1998,24 @@ public class ServerHttpSecurity {
}
}
private ServerMaximumSessionsExceededHandler getMaximumSessionsExceededHandler() {
if (this.maximumSessionsExceededHandler != null) {
return this.maximumSessionsExceededHandler;
}
DefaultWebSessionManager webSessionManager = getBeanOrNull(
WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME, DefaultWebSessionManager.class);
if (webSessionManager != null) {
this.maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler(
webSessionManager.getSessionStore());
}
if (this.maximumSessionsExceededHandler == null) {
throw new IllegalStateException(
"Could not create a default ServerMaximumSessionsExceededHandler. Please provide "
+ "a ServerMaximumSessionsExceededHandler via DSL");
}
return this.maximumSessionsExceededHandler;
}
private void configureSuccessHandlerOnAuthenticationFilters() {
if (ServerHttpSecurity.this.formLogin != null) {
ServerHttpSecurity.this.formLogin.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);

View File

@ -34,9 +34,8 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFlux
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
import org.springframework.security.core.session.ReactiveSessionInformation;
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry;
import org.springframework.security.core.session.ReactiveSessionRegistry;
import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
@ -52,10 +51,8 @@ import org.springframework.security.web.server.authentication.InvalidateLeastUse
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.SessionLimit;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.session.WebSessionStoreReactiveSessionRegistry;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
@ -322,45 +319,6 @@ public class SessionManagementSpecTests {
// @formatter:on
}
@Test
void loginWhenUnlimitedSessionsButSessionsInvalidatedManuallyThenInvalidates() {
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.UNLIMITED;
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
ResponseCookie firstLogin = loginReturningCookie(data);
ResponseCookie secondLogin = loginReturningCookie(data);
this.client.get().uri("/").cookie(firstLogin.getName(), firstLogin.getValue()).exchange().expectStatus().isOk();
this.client.get()
.uri("/")
.cookie(secondLogin.getName(), secondLogin.getValue())
.exchange()
.expectStatus()
.isOk();
ReactiveSessionRegistry sessionRegistry = this.spring.getContext().getBean(ReactiveSessionRegistry.class);
sessionRegistry.getAllSessions(PasswordEncodedUser.user())
.flatMap(ReactiveSessionInformation::invalidate)
.blockLast();
this.client.get()
.uri("/")
.cookie(firstLogin.getName(), firstLogin.getValue())
.exchange()
.expectStatus()
.isFound()
.expectHeader()
.location("/login");
this.client.get()
.uri("/")
.cookie(secondLogin.getName(), secondLogin.getValue())
.exchange()
.expectStatus()
.isFound()
.expectHeader()
.location("/login");
}
@Test
void oauth2LoginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
@ -490,10 +448,9 @@ public class SessionManagementSpecTests {
ServerOAuth2AuthorizationRequestResolver resolver = mock(ServerOAuth2AuthorizationRequestResolver.class);
ServerAuthenticationSuccessHandler successHandler = mock(ServerAuthenticationSuccessHandler.class);
@Bean
SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) {
SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http,
DefaultWebSessionManager webSessionManager) {
// @formatter:off
http
.authorizeExchange((exchanges) -> exchanges
@ -509,7 +466,7 @@ public class SessionManagementSpecTests {
.maximumSessions(SessionLimit.of(maxSessions))
.maximumSessionsExceededHandler(preventLogin
? new PreventLoginServerMaximumSessionsExceededHandler()
: new InvalidateLeastUsedServerMaximumSessionsExceededHandler())
: new InvalidateLeastUsedServerMaximumSessionsExceededHandler(webSessionManager.getSessionStore()))
)
);
// @formatter:on
@ -611,8 +568,8 @@ public class SessionManagementSpecTests {
}
@Bean
ReactiveSessionRegistry reactiveSessionRegistry(DefaultWebSessionManager webSessionManager) {
return new WebSessionStoreReactiveSessionRegistry(webSessionManager.getSessionStore());
ReactiveSessionRegistry reactiveSessionRegistry() {
return new InMemoryReactiveSessionRegistry();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -29,13 +29,13 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFlux
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry
import org.springframework.security.core.session.ReactiveSessionRegistry
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler
import org.springframework.security.web.server.authentication.SessionLimit
import org.springframework.security.web.session.WebSessionStoreReactiveSessionRegistry
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
@ -45,7 +45,6 @@ import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
import org.springframework.web.server.session.DefaultWebSessionManager
import reactor.core.publisher.Mono
/**
* Tests for [ServerSessionManagementDsl]
@ -208,7 +207,7 @@ class ServerSessionManagementDslTests {
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
open fun springSecurity(http: ServerHttpSecurity, webSessionManager: DefaultWebSessionManager): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
@ -217,7 +216,7 @@ class ServerSessionManagementDslTests {
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(maxSessions)
maximumSessionsExceededHandler = InvalidateLeastUsedServerMaximumSessionsExceededHandler()
maximumSessionsExceededHandler = InvalidateLeastUsedServerMaximumSessionsExceededHandler(webSessionManager.sessionStore)
}
}
}
@ -263,8 +262,8 @@ class ServerSessionManagementDslTests {
}
@Bean
open fun reactiveSessionRegistry(webSessionManager: DefaultWebSessionManager): ReactiveSessionRegistry {
return WebSessionStoreReactiveSessionRegistry(webSessionManager.sessionStore)
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
return InMemoryReactiveSessionRegistry()
}
}

View File

@ -28,246 +28,6 @@ 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(WebSessionManager webSessionManager) {
return new WebSessionStoreReactiveSessionRegistry(((DefaultWebSessionManager) webSessionManager).getSessionStore());
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(1)
}
}
}
}
@Bean
open fun reactiveSessionRegistry(webSessionManager: WebSessionManager): ReactiveSessionRegistry {
return WebSessionStoreReactiveSessionRegistry((webSessionManager as DefaultWebSessionManager).sessionStore)
}
----
======
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(WebSessionManager webSessionManager) {
return new WebSessionStoreReactiveSessionRegistry(((DefaultWebSessionManager) webSessionManager).getSessionStore());
}
----
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 WebSessionStoreReactiveSessionRegistry((webSessionManager as DefaultWebSessionManager).sessionStore)
}
----
======
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(WebSessionManager webSessionManager) {
return new WebSessionStoreReactiveSessionRegistry(((DefaultWebSessionManager) webSessionManager).getSessionStore());
}
----
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(webSessionManager: WebSessionManager): ReactiveSessionRegistry {
return WebSessionStoreReactiveSessionRegistry((webSessionManager as DefaultWebSessionManager).sessionStore)
}
----
======
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>>.
[[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 {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
[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(WebSessionManager webSessionManager) {
return new WebSessionStoreReactiveSessionRegistry(((DefaultWebSessionManager) webSessionManager).getSessionStore());
}
----
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(webSessionManager: WebSessionManager): ReactiveSessionRegistry {
return WebSessionStoreReactiveSessionRegistry((webSessionManager as DefaultWebSessionManager).sessionStore)
}
----
======
[[reactive-concurrent-sessions-control-specify-session-registry]]
== Specifying a `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.
Typically, in a Spring WebFlux application, you will use the {security-api-url}/org/springframework/security/web/session/WebSessionStoreReactiveSessionRegistry.html[WebSessionStoreReactiveSessionRegistry] which makes sure that the `WebSession` is invalidated whenever the `ReactiveSessionInformation` is invalidated.
Spring Security ships with {security-api-url}/org/springframework/security/web/session/WebSessionStoreReactiveSessionRegistry.html[WebSessionStoreReactiveSessionRegistry] and {security-api-url}org/springframework/security/core/session/InMemoryReactiveSessionRegistry.html[InMemoryReactiveSessionRegistry] implementations of `ReactiveSessionRegistry`.
[NOTE]
====
When creating the `WebSessionStoreReactiveSessionRegistry`, you need to provide the `WebSessionStore` that is being used by your application.
If you are using Spring WebFlux, you can use the `WebSessionManager` bean (which is usually an instance of `DefaultWebSessionManager`) to get the `WebSessionStore`.
====
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
// ...
@ -300,6 +60,123 @@ open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
}
}
}
@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 {
@ -308,6 +185,123 @@ open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
----
======
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>>.
[[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 {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
[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 {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 ships with {security-api-url}org/springframework/security/core/session/InMemoryReactiveSessionRegistry.html[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
@ -324,7 +318,7 @@ SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(SessionLimit.of(1))
.sessionRegistry(new InMemoryReactiveSessionRegistry())
.sessionRegistry(new MyReactiveSessionRegistry())
)
);
return http.build();
@ -342,7 +336,7 @@ open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(1)
sessionRegistry = InMemoryReactiveSessionRegistry()
sessionRegistry = MyReactiveSessionRegistry()
}
}
}
@ -355,7 +349,7 @@ open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
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 and then invalidate them:
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]
@ -367,13 +361,12 @@ Java::
public class SessionControl {
private final ReactiveSessionRegistry reactiveSessionRegistry;
public SessionControl(ReactiveSessionRegistry reactiveSessionRegistry) {
this.reactiveSessionRegistry = reactiveSessionRegistry;
}
private final WebSessionStore webSessionStore;
public Mono<Void> invalidateSessions(String username) {
return this.reactiveSessionRegistry.getAllSessions(username)
.flatMap(ReactiveSessionInformation::invalidate)
.flatMap((session) -> session.invalidate().thenReturn(session))
.flatMap((session) -> this.webSessionStore.removeSession(session.getSessionId()))
.then();
}
}

View File

@ -45,13 +45,16 @@ public final class ConcurrentSessionControlServerAuthenticationSuccessHandler
private final ReactiveSessionRegistry sessionRegistry;
private final ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler;
private SessionLimit sessionLimit = SessionLimit.of(1);
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler();
public ConcurrentSessionControlServerAuthenticationSuccessHandler(ReactiveSessionRegistry sessionRegistry) {
public ConcurrentSessionControlServerAuthenticationSuccessHandler(ReactiveSessionRegistry sessionRegistry,
ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
Assert.notNull(maximumSessionsExceededHandler, "maximumSessionsExceededHandler cannot be null");
this.sessionRegistry = sessionRegistry;
this.maximumSessionsExceededHandler = maximumSessionsExceededHandler;
}
@Override
@ -97,15 +100,4 @@ public final class ConcurrentSessionControlServerAuthenticationSuccessHandler
this.sessionLimit = sessionLimit;
}
/**
* Sets the {@link ServerMaximumSessionsExceededHandler} to use. The default is
* {@link InvalidateLeastUsedServerMaximumSessionsExceededHandler}.
* @param maximumSessionsExceededHandler the
* {@link ServerMaximumSessionsExceededHandler} to use
*/
public void setMaximumSessionsExceededHandler(ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler) {
Assert.notNull(maximumSessionsExceededHandler, "maximumSessionsExceededHandler cannot be null");
this.maximumSessionsExceededHandler = maximumSessionsExceededHandler;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,19 +20,18 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.session.ReactiveSessionInformation;
import org.springframework.web.server.session.WebSessionStore;
/**
* Implementation of {@link ServerMaximumSessionsExceededHandler} that invalidates the
* least recently used session(s). It only invalidates the amount of sessions that exceed
* the maximum allowed. For example, if the maximum was exceeded by 1, only the least
* recently used session will be invalidated.
* least recently used {@link ReactiveSessionInformation} and removes the related sessions
* from the {@link WebSessionStore}. It only invalidates the amount of sessions that
* exceed the maximum allowed. For example, if the maximum was exceeded by 1, only the
* least recently used session will be invalidated.
*
* @author Marcus da Coregio
* @since 6.3
@ -40,7 +39,11 @@ import org.springframework.security.core.session.ReactiveSessionInformation;
public final class InvalidateLeastUsedServerMaximumSessionsExceededHandler
implements ServerMaximumSessionsExceededHandler {
private final Log logger = LogFactory.getLog(getClass());
private final WebSessionStore webSessionStore;
public InvalidateLeastUsedServerMaximumSessionsExceededHandler(WebSessionStore webSessionStore) {
this.webSessionStore = webSessionStore;
}
@Override
public Mono<Void> handle(MaximumSessionsContext context) {
@ -51,10 +54,8 @@ public final class InvalidateLeastUsedServerMaximumSessionsExceededHandler
maximumSessionsExceededBy);
return Flux.fromIterable(leastRecentlyUsedSessionsToInvalidate)
.doOnComplete(() -> this.logger
.debug(LogMessage.format("Invalidated %d least recently used sessions for authentication %s",
leastRecentlyUsedSessionsToInvalidate.size(), context.getAuthentication().getName())))
.flatMap(ReactiveSessionInformation::invalidate)
.flatMap((toInvalidate) -> toInvalidate.invalidate().thenReturn(toInvalidate))
.flatMap((toInvalidate) -> this.webSessionStore.removeSession(toInvalidate.getSessionId()))
.then();
}

View File

@ -1,100 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.session;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry;
import org.springframework.security.core.session.ReactiveSessionInformation;
import org.springframework.security.core.session.ReactiveSessionRegistry;
import org.springframework.util.Assert;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.session.WebSessionStore;
/**
* A {@link ReactiveSessionRegistry} implementation that uses a {@link WebSessionStore} to
* invalidate a {@link WebSession} when the {@link ReactiveSessionInformation} is
* invalidated.
*
* @author Marcus da Coregio
* @since 6.3
*/
public final class WebSessionStoreReactiveSessionRegistry implements ReactiveSessionRegistry {
private final WebSessionStore webSessionStore;
private ReactiveSessionRegistry sessionRegistry = new InMemoryReactiveSessionRegistry();
public WebSessionStoreReactiveSessionRegistry(WebSessionStore webSessionStore) {
Assert.notNull(webSessionStore, "webSessionStore cannot be null");
this.webSessionStore = webSessionStore;
}
@Override
public Flux<ReactiveSessionInformation> getAllSessions(Object principal) {
return this.sessionRegistry.getAllSessions(principal).map(WebSessionInformation::new);
}
@Override
public Mono<Void> saveSessionInformation(ReactiveSessionInformation information) {
return this.sessionRegistry.saveSessionInformation(new WebSessionInformation(information));
}
@Override
public Mono<ReactiveSessionInformation> getSessionInformation(String sessionId) {
return this.sessionRegistry.getSessionInformation(sessionId).map(WebSessionInformation::new);
}
@Override
public Mono<ReactiveSessionInformation> removeSessionInformation(String sessionId) {
return this.sessionRegistry.removeSessionInformation(sessionId).map(WebSessionInformation::new);
}
@Override
public Mono<ReactiveSessionInformation> updateLastAccessTime(String sessionId) {
return this.sessionRegistry.updateLastAccessTime(sessionId).map(WebSessionInformation::new);
}
/**
* Sets the {@link ReactiveSessionRegistry} to use.
* @param sessionRegistry the {@link ReactiveSessionRegistry} to use. Cannot be null.
*/
public void setSessionRegistry(ReactiveSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
final class WebSessionInformation extends ReactiveSessionInformation {
WebSessionInformation(ReactiveSessionInformation sessionInformation) {
super(sessionInformation.getPrincipal(), sessionInformation.getSessionId(),
sessionInformation.getLastAccessTime());
}
@Override
public Mono<Void> invalidate() {
return WebSessionStoreReactiveSessionRegistry.this.webSessionStore.retrieveSession(getSessionId())
.flatMap(WebSession::invalidate)
.then(Mono
.defer(() -> WebSessionStoreReactiveSessionRegistry.this.removeSessionInformation(getSessionId())))
.then(Mono.defer(super::invalidate));
}
}
}

View File

@ -74,34 +74,36 @@ class ConcurrentSessionControlServerAuthenticationSuccessHandlerTests {
given(this.exchange.getRequest()).willReturn(MockServerHttpRequest.get("/").build());
given(this.exchange.getSession()).willReturn(Mono.just(new MockWebSession()));
given(this.handler.handle(any())).willReturn(Mono.empty());
this.strategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(this.sessionRegistry);
this.strategy.setMaximumSessionsExceededHandler(this.handler);
this.strategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(this.sessionRegistry,
this.handler);
}
@Test
void constructorWhenNullRegistryThenException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ConcurrentSessionControlServerAuthenticationSuccessHandler(null))
.isThrownBy(() -> new ConcurrentSessionControlServerAuthenticationSuccessHandler(null, this.handler))
.withMessage("sessionRegistry cannot be null");
}
@Test
void constructorWhenNullHandlerThenException() {
assertThatIllegalArgumentException()
.isThrownBy(
() -> new ConcurrentSessionControlServerAuthenticationSuccessHandler(this.sessionRegistry, null))
.withMessage("maximumSessionsExceededHandler cannot be null");
}
@Test
void setMaximumSessionsForAuthenticationWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.strategy.setSessionLimit(null))
.withMessage("sessionLimit cannot be null");
}
@Test
void setMaximumSessionsExceededHandlerWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.strategy.setMaximumSessionsExceededHandler(null))
.withMessage("maximumSessionsExceededHandler cannot be null");
}
@Test
void onAuthenticationWhenSessionLimitIsUnlimitedThenDoNothing() {
ServerMaximumSessionsExceededHandler handler = mock(ServerMaximumSessionsExceededHandler.class);
this.strategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(this.sessionRegistry, handler);
this.strategy.setSessionLimit(SessionLimit.UNLIMITED);
this.strategy.setMaximumSessionsExceededHandler(handler);
this.strategy.onAuthenticationSuccess(null, TestAuthentication.authenticatedUser()).block();
verifyNoInteractions(handler, this.sessionRegistry);
}

View File

@ -19,6 +19,7 @@ package org.springframework.security.web.server.authentication.session;
import java.time.Instant;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
@ -26,10 +27,13 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.ReactiveSessionInformation;
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.MaximumSessionsContext;
import org.springframework.web.server.session.InMemoryWebSessionStore;
import org.springframework.web.server.session.WebSessionStore;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@ -40,7 +44,14 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
*/
class InvalidateLeastUsedServerMaximumSessionsExceededHandlerTests {
InvalidateLeastUsedServerMaximumSessionsExceededHandler handler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler();
InvalidateLeastUsedServerMaximumSessionsExceededHandler handler;
WebSessionStore webSessionStore = spy(new InMemoryWebSessionStore());
@BeforeEach
void setup() {
this.handler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler(this.webSessionStore);
}
@Test
void handleWhenInvokedThenInvalidatesLeastRecentlyUsedSessions() {
@ -48,7 +59,9 @@ class InvalidateLeastUsedServerMaximumSessionsExceededHandlerTests {
ReactiveSessionInformation session2 = mock(ReactiveSessionInformation.class);
given(session1.getLastAccessTime()).willReturn(Instant.ofEpochMilli(1700827760010L));
given(session2.getLastAccessTime()).willReturn(Instant.ofEpochMilli(1700827760000L));
given(session2.getSessionId()).willReturn("session2");
given(session2.invalidate()).willReturn(Mono.empty());
MaximumSessionsContext context = new MaximumSessionsContext(mock(Authentication.class),
List.of(session1, session2), 2, null);
@ -57,6 +70,10 @@ class InvalidateLeastUsedServerMaximumSessionsExceededHandlerTests {
verify(session2).invalidate();
verify(session1).getLastAccessTime(); // used by comparator to sort the sessions
verify(session2).getLastAccessTime(); // used by comparator to sort the sessions
verify(session2).getSessionId(); // used to invalidate session against the
// WebSessionStore
verify(this.webSessionStore).removeSession("session2");
verifyNoMoreInteractions(this.webSessionStore);
verifyNoMoreInteractions(session2);
verifyNoMoreInteractions(session1);
}
@ -71,17 +88,24 @@ class InvalidateLeastUsedServerMaximumSessionsExceededHandlerTests {
given(session3.getLastAccessTime()).willReturn(Instant.ofEpochMilli(1700827760030L));
given(session1.invalidate()).willReturn(Mono.empty());
given(session2.invalidate()).willReturn(Mono.empty());
given(session1.getSessionId()).willReturn("session1");
given(session2.getSessionId()).willReturn("session2");
MaximumSessionsContext context = new MaximumSessionsContext(mock(Authentication.class),
List.of(session1, session2, session3), 2, null);
this.handler.handle(context).block();
// @formatter:off
verify(session1).invalidate();
verify(session2).invalidate();
verify(session1).getSessionId();
verify(session2).getSessionId();
verify(session1, atLeastOnce()).getLastAccessTime(); // used by comparator to sort the sessions
verify(session2, atLeastOnce()).getLastAccessTime(); // used by comparator to sort the sessions
verify(session3, atLeastOnce()).getLastAccessTime(); // used by comparator to sort the sessions
verify(this.webSessionStore).removeSession("session1");
verify(this.webSessionStore).removeSession("session2");
verifyNoMoreInteractions(this.webSessionStore);
verifyNoMoreInteractions(session1);
verifyNoMoreInteractions(session2);
verifyNoMoreInteractions(session3);

View File

@ -1,136 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.session;
import java.time.Instant;
import java.util.List;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.security.core.session.ReactiveSessionInformation;
import org.springframework.security.core.session.ReactiveSessionRegistry;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.session.WebSessionStore;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link WebSessionStoreReactiveSessionRegistry}
*
* @author Marcus da Coregio
*/
class WebSessionStoreReactiveSessionRegistryTests {
WebSessionStore webSessionStore = mock();
WebSessionStoreReactiveSessionRegistry registry = new WebSessionStoreReactiveSessionRegistry(this.webSessionStore);
@Test
void constructorWhenWebSessionStoreNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> new WebSessionStoreReactiveSessionRegistry(null))
.withMessage("webSessionStore cannot be null");
}
@Test
void getSessionInformationWhenSavedThenReturnsWebSessionInformation() {
ReactiveSessionInformation session = createSession();
this.registry.saveSessionInformation(session).block();
ReactiveSessionInformation saved = this.registry.getSessionInformation(session.getSessionId()).block();
assertThat(saved).isInstanceOf(WebSessionStoreReactiveSessionRegistry.WebSessionInformation.class);
assertThat(saved.getPrincipal()).isEqualTo(session.getPrincipal());
assertThat(saved.getSessionId()).isEqualTo(session.getSessionId());
assertThat(saved.getLastAccessTime()).isEqualTo(session.getLastAccessTime());
}
@Test
void invalidateWhenReturnedFromGetSessionInformationThenWebSessionInvalidatedAndRemovedFromRegistry() {
ReactiveSessionInformation session = createSession();
WebSession webSession = mock();
given(webSession.invalidate()).willReturn(Mono.empty());
given(this.webSessionStore.retrieveSession(session.getSessionId())).willReturn(Mono.just(webSession));
this.registry.saveSessionInformation(session).block();
ReactiveSessionInformation saved = this.registry.getSessionInformation(session.getSessionId()).block();
saved.invalidate().block();
verify(webSession).invalidate();
assertThat(this.registry.getSessionInformation(saved.getSessionId()).block()).isNull();
}
@Test
void invalidateWhenReturnedFromRemoveSessionInformationThenWebSessionInvalidatedAndRemovedFromRegistry() {
ReactiveSessionInformation session = createSession();
WebSession webSession = mock();
given(webSession.invalidate()).willReturn(Mono.empty());
given(this.webSessionStore.retrieveSession(session.getSessionId())).willReturn(Mono.just(webSession));
this.registry.saveSessionInformation(session).block();
ReactiveSessionInformation saved = this.registry.removeSessionInformation(session.getSessionId()).block();
saved.invalidate().block();
verify(webSession).invalidate();
assertThat(this.registry.getSessionInformation(saved.getSessionId()).block()).isNull();
}
@Test
void invalidateWhenReturnedFromGetAllSessionsThenWebSessionInvalidatedAndRemovedFromRegistry() {
ReactiveSessionInformation session = createSession();
WebSession webSession = mock();
given(webSession.invalidate()).willReturn(Mono.empty());
given(this.webSessionStore.retrieveSession(session.getSessionId())).willReturn(Mono.just(webSession));
this.registry.saveSessionInformation(session).block();
List<ReactiveSessionInformation> saved = this.registry.getAllSessions(session.getPrincipal())
.collectList()
.block();
saved.forEach((info) -> info.invalidate().block());
verify(webSession).invalidate();
assertThat(this.registry.getAllSessions(session.getPrincipal()).collectList().block()).isEmpty();
}
@Test
void setSessionRegistryThenUses() {
ReactiveSessionRegistry sessionRegistry = mock();
given(sessionRegistry.saveSessionInformation(any())).willReturn(Mono.empty());
given(sessionRegistry.removeSessionInformation(any())).willReturn(Mono.empty());
given(sessionRegistry.updateLastAccessTime(any())).willReturn(Mono.empty());
given(sessionRegistry.getSessionInformation(any())).willReturn(Mono.empty());
given(sessionRegistry.getAllSessions(any())).willReturn(Flux.empty());
this.registry.setSessionRegistry(sessionRegistry);
ReactiveSessionInformation session = createSession();
this.registry.saveSessionInformation(session).block();
verify(sessionRegistry).saveSessionInformation(any());
this.registry.removeSessionInformation(session.getSessionId()).block();
verify(sessionRegistry).removeSessionInformation(any());
this.registry.updateLastAccessTime(session.getSessionId()).block();
verify(sessionRegistry).updateLastAccessTime(any());
this.registry.getSessionInformation(session.getSessionId()).block();
verify(sessionRegistry).getSessionInformation(any());
this.registry.getAllSessions(session.getPrincipal()).blockFirst();
verify(sessionRegistry).getAllSessions(any());
}
private static ReactiveSessionInformation createSession() {
return new ReactiveSessionInformation("principal", "sessionId", Instant.now());
}
}