mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-31 09:12:14 +00:00
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:
parent
f3bcf7ed5d
commit
f8ff056eb6
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user