1
0
mirror of synced 2026-08-06 02:08:01 +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:
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
@@ -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();
}
}