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