Skip to content

Commit f8ff056

Browse files
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
1 parent f3bcf7e commit f8ff056

File tree

10 files changed

+116
-365
lines changed

10 files changed

+116
-365
lines changed

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@
214214
import org.springframework.web.server.WebFilter;
215215
import org.springframework.web.server.WebFilterChain;
216216
import org.springframework.web.server.WebSession;
217+
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
218+
import org.springframework.web.server.session.DefaultWebSessionManager;
217219
import org.springframework.web.util.pattern.PathPatternParser;
218220

219221
/**
@@ -1964,7 +1966,7 @@ public class SessionManagementSpec {
19641966

19651967
private SessionLimit sessionLimit = SessionLimit.UNLIMITED;
19661968

1967-
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler();
1969+
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler;
19681970

19691971
/**
19701972
* Configures how many sessions are allowed for a given user.
@@ -1983,9 +1985,8 @@ void configure(ServerHttpSecurity http) {
19831985
if (this.concurrentSessions != null) {
19841986
ReactiveSessionRegistry reactiveSessionRegistry = getSessionRegistry();
19851987
ConcurrentSessionControlServerAuthenticationSuccessHandler concurrentSessionControlStrategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(
1986-
reactiveSessionRegistry);
1988+
reactiveSessionRegistry, getMaximumSessionsExceededHandler());
19871989
concurrentSessionControlStrategy.setSessionLimit(this.sessionLimit);
1988-
concurrentSessionControlStrategy.setMaximumSessionsExceededHandler(this.maximumSessionsExceededHandler);
19891990
RegisterSessionServerAuthenticationSuccessHandler registerSessionAuthenticationStrategy = new RegisterSessionServerAuthenticationSuccessHandler(
19901991
reactiveSessionRegistry);
19911992
this.authenticationSuccessHandler = new DelegatingServerAuthenticationSuccessHandler(
@@ -1997,6 +1998,24 @@ void configure(ServerHttpSecurity http) {
19971998
}
19981999
}
19992000

2001+
private ServerMaximumSessionsExceededHandler getMaximumSessionsExceededHandler() {
2002+
if (this.maximumSessionsExceededHandler != null) {
2003+
return this.maximumSessionsExceededHandler;
2004+
}
2005+
DefaultWebSessionManager webSessionManager = getBeanOrNull(
2006+
WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME, DefaultWebSessionManager.class);
2007+
if (webSessionManager != null) {
2008+
this.maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler(
2009+
webSessionManager.getSessionStore());
2010+
}
2011+
if (this.maximumSessionsExceededHandler == null) {
2012+
throw new IllegalStateException(
2013+
"Could not create a default ServerMaximumSessionsExceededHandler. Please provide "
2014+
+ "a ServerMaximumSessionsExceededHandler via DSL");
2015+
}
2016+
return this.maximumSessionsExceededHandler;
2017+
}
2018+
20002019
private void configureSuccessHandlerOnAuthenticationFilters() {
20012020
if (ServerHttpSecurity.this.formLogin != null) {
20022021
ServerHttpSecurity.this.formLogin.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);

config/src/test/java/org/springframework/security/config/web/server/SessionManagementSpecTests.java

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@
3434
import org.springframework.security.config.test.SpringTestContext;
3535
import org.springframework.security.config.test.SpringTestContextExtension;
3636
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
37-
import org.springframework.security.core.session.ReactiveSessionInformation;
37+
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry;
3838
import org.springframework.security.core.session.ReactiveSessionRegistry;
39-
import org.springframework.security.core.userdetails.PasswordEncodedUser;
4039
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
4140
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
4241
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
@@ -52,10 +51,8 @@
5251
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler;
5352
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
5453
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
55-
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
5654
import org.springframework.security.web.server.authentication.SessionLimit;
5755
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
58-
import org.springframework.security.web.session.WebSessionStoreReactiveSessionRegistry;
5956
import org.springframework.test.context.junit.jupiter.SpringExtension;
6057
import org.springframework.test.web.reactive.server.WebTestClient;
6158
import org.springframework.util.LinkedMultiValueMap;
@@ -322,45 +319,6 @@ void oauth2LoginWhenMaxSessionsThenPreventLogin() {
322319
// @formatter:on
323320
}
324321

325-
@Test
326-
void loginWhenUnlimitedSessionsButSessionsInvalidatedManuallyThenInvalidates() {
327-
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.UNLIMITED;
328-
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
329-
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
330-
data.add("username", "user");
331-
data.add("password", "password");
332-
333-
ResponseCookie firstLogin = loginReturningCookie(data);
334-
ResponseCookie secondLogin = loginReturningCookie(data);
335-
this.client.get().uri("/").cookie(firstLogin.getName(), firstLogin.getValue()).exchange().expectStatus().isOk();
336-
this.client.get()
337-
.uri("/")
338-
.cookie(secondLogin.getName(), secondLogin.getValue())
339-
.exchange()
340-
.expectStatus()
341-
.isOk();
342-
ReactiveSessionRegistry sessionRegistry = this.spring.getContext().getBean(ReactiveSessionRegistry.class);
343-
sessionRegistry.getAllSessions(PasswordEncodedUser.user())
344-
.flatMap(ReactiveSessionInformation::invalidate)
345-
.blockLast();
346-
this.client.get()
347-
.uri("/")
348-
.cookie(firstLogin.getName(), firstLogin.getValue())
349-
.exchange()
350-
.expectStatus()
351-
.isFound()
352-
.expectHeader()
353-
.location("/login");
354-
this.client.get()
355-
.uri("/")
356-
.cookie(secondLogin.getName(), secondLogin.getValue())
357-
.exchange()
358-
.expectStatus()
359-
.isFound()
360-
.expectHeader()
361-
.location("/login");
362-
}
363-
364322
@Test
365323
void oauth2LoginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
366324
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
@@ -490,10 +448,9 @@ static class OAuth2LoginConcurrentSessionsConfig {
490448

491449
ServerOAuth2AuthorizationRequestResolver resolver = mock(ServerOAuth2AuthorizationRequestResolver.class);
492450

493-
ServerAuthenticationSuccessHandler successHandler = mock(ServerAuthenticationSuccessHandler.class);
494-
495451
@Bean
496-
SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) {
452+
SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http,
453+
DefaultWebSessionManager webSessionManager) {
497454
// @formatter:off
498455
http
499456
.authorizeExchange((exchanges) -> exchanges
@@ -509,7 +466,7 @@ SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) {
509466
.maximumSessions(SessionLimit.of(maxSessions))
510467
.maximumSessionsExceededHandler(preventLogin
511468
? new PreventLoginServerMaximumSessionsExceededHandler()
512-
: new InvalidateLeastUsedServerMaximumSessionsExceededHandler())
469+
: new InvalidateLeastUsedServerMaximumSessionsExceededHandler(webSessionManager.getSessionStore()))
513470
)
514471
);
515472
// @formatter:on
@@ -611,8 +568,8 @@ DefaultWebSessionManager webSessionManager() {
611568
}
612569

613570
@Bean
614-
ReactiveSessionRegistry reactiveSessionRegistry(DefaultWebSessionManager webSessionManager) {
615-
return new WebSessionStoreReactiveSessionRegistry(webSessionManager.getSessionStore());
571+
ReactiveSessionRegistry reactiveSessionRegistry() {
572+
return new InMemoryReactiveSessionRegistry();
616573
}
617574

618575
}

config/src/test/kotlin/org/springframework/security/config/web/server/ServerSessionManagementDslTests.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* 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
2929
import org.springframework.security.config.test.SpringTestContext
3030
import org.springframework.security.config.test.SpringTestContextExtension
3131
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration
32+
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry
3233
import org.springframework.security.core.session.ReactiveSessionRegistry
3334
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
3435
import org.springframework.security.web.server.SecurityWebFilterChain
3536
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler
3637
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler
3738
import org.springframework.security.web.server.authentication.SessionLimit
38-
import org.springframework.security.web.session.WebSessionStoreReactiveSessionRegistry
3939
import org.springframework.test.web.reactive.server.WebTestClient
4040
import org.springframework.util.LinkedMultiValueMap
4141
import org.springframework.util.MultiValueMap
@@ -45,7 +45,6 @@ import org.springframework.web.reactive.config.EnableWebFlux
4545
import org.springframework.web.reactive.function.BodyInserters
4646
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
4747
import org.springframework.web.server.session.DefaultWebSessionManager
48-
import reactor.core.publisher.Mono
4948

5049
/**
5150
* Tests for [ServerSessionManagementDsl]
@@ -208,7 +207,7 @@ class ServerSessionManagementDslTests {
208207
}
209208

210209
@Bean
211-
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
210+
open fun springSecurity(http: ServerHttpSecurity, webSessionManager: DefaultWebSessionManager): SecurityWebFilterChain {
212211
return http {
213212
authorizeExchange {
214213
authorize(anyExchange, authenticated)
@@ -217,7 +216,7 @@ class ServerSessionManagementDslTests {
217216
sessionManagement {
218217
sessionConcurrency {
219218
maximumSessions = SessionLimit.of(maxSessions)
220-
maximumSessionsExceededHandler = InvalidateLeastUsedServerMaximumSessionsExceededHandler()
219+
maximumSessionsExceededHandler = InvalidateLeastUsedServerMaximumSessionsExceededHandler(webSessionManager.sessionStore)
221220
}
222221
}
223222
}
@@ -263,8 +262,8 @@ class ServerSessionManagementDslTests {
263262
}
264263

265264
@Bean
266-
open fun reactiveSessionRegistry(webSessionManager: DefaultWebSessionManager): ReactiveSessionRegistry {
267-
return WebSessionStoreReactiveSessionRegistry(webSessionManager.sessionStore)
265+
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
266+
return InMemoryReactiveSessionRegistry()
268267
}
269268

270269
}

docs/modules/ROOT/pages/reactive/authentication/concurrent-sessions-control.adoc

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
3434
.sessionManagement((sessions) -> sessions
3535
.concurrentSessions((concurrency) -> concurrency
3636
.maximumSessions(SessionLimit.of(1))
37+
)
3738
);
3839
return http.build();
3940
}
4041
4142
@Bean
42-
ReactiveSessionRegistry reactiveSessionRegistry(WebSessionManager webSessionManager) {
43-
return new WebSessionStoreReactiveSessionRegistry(((DefaultWebSessionManager) webSessionManager).getSessionStore());
43+
ReactiveSessionRegistry reactiveSessionRegistry() {
44+
return new InMemoryReactiveSessionRegistry();
4445
}
4546
----
4647
@@ -60,8 +61,8 @@ open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
6061
}
6162
}
6263
@Bean
63-
open fun reactiveSessionRegistry(webSessionManager: WebSessionManager): ReactiveSessionRegistry {
64-
return WebSessionStoreReactiveSessionRegistry((webSessionManager as DefaultWebSessionManager).sessionStore)
64+
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
65+
return InMemoryReactiveSessionRegistry()
6566
}
6667
----
6768
======
@@ -88,8 +89,8 @@ SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
8889
}
8990
9091
@Bean
91-
ReactiveSessionRegistry reactiveSessionRegistry(WebSessionManager webSessionManager) {
92-
return new WebSessionStoreReactiveSessionRegistry(((DefaultWebSessionManager) webSessionManager).getSessionStore());
92+
ReactiveSessionRegistry reactiveSessionRegistry() {
93+
return new InMemoryReactiveSessionRegistry();
9394
}
9495
----
9596
@@ -110,7 +111,7 @@ open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
110111
}
111112
@Bean
112113
open fun reactiveSessionRegistry(webSessionManager: WebSessionManager): ReactiveSessionRegistry {
113-
return WebSessionStoreReactiveSessionRegistry((webSessionManager as DefaultWebSessionManager).sessionStore)
114+
return InMemoryReactiveSessionRegistry()
114115
}
115116
----
116117
======
@@ -148,8 +149,8 @@ private SessionLimit maxSessions() {
148149
}
149150
150151
@Bean
151-
ReactiveSessionRegistry reactiveSessionRegistry(WebSessionManager webSessionManager) {
152-
return new WebSessionStoreReactiveSessionRegistry(((DefaultWebSessionManager) webSessionManager).getSessionStore());
152+
ReactiveSessionRegistry reactiveSessionRegistry() {
153+
return new InMemoryReactiveSessionRegistry();
153154
}
154155
----
155156
@@ -178,8 +179,8 @@ fun maxSessions(): SessionLimit {
178179
}
179180
180181
@Bean
181-
open fun reactiveSessionRegistry(webSessionManager: WebSessionManager): ReactiveSessionRegistry {
182-
return WebSessionStoreReactiveSessionRegistry((webSessionManager as DefaultWebSessionManager).sessionStore)
182+
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
183+
return InMemoryReactiveSessionRegistry()
183184
}
184185
----
185186
======
@@ -215,8 +216,8 @@ SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
215216
}
216217
217218
@Bean
218-
ReactiveSessionRegistry reactiveSessionRegistry(WebSessionManager webSessionManager) {
219-
return new WebSessionStoreReactiveSessionRegistry(((DefaultWebSessionManager) webSessionManager).getSessionStore());
219+
ReactiveSessionRegistry reactiveSessionRegistry() {
220+
return new InMemoryReactiveSessionRegistry();
220221
}
221222
----
222223
@@ -238,8 +239,8 @@ open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
238239
}
239240
240241
@Bean
241-
open fun reactiveSessionRegistry(webSessionManager: WebSessionManager): ReactiveSessionRegistry {
242-
return WebSessionStoreReactiveSessionRegistry((webSessionManager as DefaultWebSessionManager).sessionStore)
242+
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
243+
return InMemoryReactiveSessionRegistry()
243244
}
244245
----
245246
======
@@ -248,15 +249,8 @@ open fun reactiveSessionRegistry(webSessionManager: WebSessionManager): Reactive
248249
== Specifying a `ReactiveSessionRegistry`
249250

250251
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.
251-
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.
252-
253-
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`.
254252

255-
[NOTE]
256-
====
257-
When creating the `WebSessionStoreReactiveSessionRegistry`, you need to provide the `WebSessionStore` that is being used by your application.
258-
If you are using Spring WebFlux, you can use the `WebSessionManager` bean (which is usually an instance of `DefaultWebSessionManager`) to get the `WebSessionStore`.
259-
====
253+
Spring Security ships with {security-api-url}org/springframework/security/core/session/InMemoryReactiveSessionRegistry.html[InMemoryReactiveSessionRegistry] implementation of `ReactiveSessionRegistry`.
260254

261255
To specify a `ReactiveSessionRegistry` implementation you can either declare it as a bean:
262256

@@ -281,7 +275,7 @@ SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
281275
282276
@Bean
283277
ReactiveSessionRegistry reactiveSessionRegistry() {
284-
return new InMemoryReactiveSessionRegistry();
278+
return new MyReactiveSessionRegistry();
285279
}
286280
----
287281
@@ -303,7 +297,7 @@ open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
303297
304298
@Bean
305299
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
306-
return InMemoryReactiveSessionRegistry()
300+
return MyReactiveSessionRegistry()
307301
}
308302
----
309303
======
@@ -324,7 +318,7 @@ SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
324318
.sessionManagement((sessions) -> sessions
325319
.concurrentSessions((concurrency) -> concurrency
326320
.maximumSessions(SessionLimit.of(1))
327-
.sessionRegistry(new InMemoryReactiveSessionRegistry())
321+
.sessionRegistry(new MyReactiveSessionRegistry())
328322
)
329323
);
330324
return http.build();
@@ -342,7 +336,7 @@ open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
342336
sessionManagement {
343337
sessionConcurrency {
344338
maximumSessions = SessionLimit.of(1)
345-
sessionRegistry = InMemoryReactiveSessionRegistry()
339+
sessionRegistry = MyReactiveSessionRegistry()
346340
}
347341
}
348342
}
@@ -355,7 +349,7 @@ open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
355349

356350
At times, it is handy to be able to invalidate all or some of a user's sessions.
357351
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.
358-
To do that, you can use the `ReactiveSessionRegistry` bean to retrieve all the user's sessions and then invalidate them:
352+
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`:
359353

360354
.Using ReactiveSessionRegistry to invalidate sessions manually
361355
[tabs]
@@ -367,13 +361,12 @@ Java::
367361
public class SessionControl {
368362
private final ReactiveSessionRegistry reactiveSessionRegistry;
369363
370-
public SessionControl(ReactiveSessionRegistry reactiveSessionRegistry) {
371-
this.reactiveSessionRegistry = reactiveSessionRegistry;
372-
}
364+
private final WebSessionStore webSessionStore;
373365
374366
public Mono<Void> invalidateSessions(String username) {
375367
return this.reactiveSessionRegistry.getAllSessions(username)
376-
.flatMap(ReactiveSessionInformation::invalidate)
368+
.flatMap((session) -> session.invalidate().thenReturn(session))
369+
.flatMap((session) -> this.webSessionStore.removeSession(session.getSessionId()))
377370
.then();
378371
}
379372
}

0 commit comments

Comments
 (0)