1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Support Multiple ServerLogoutHandlers

This commit adds support to ServerHttpSecurity for registering
multiple ServerLogoutHandlers. This is handy so that an application
does not need to re-supply any handlers already configured by
the DSL.

Signed-off-by: blake_bauman <blake_bauman@apple.com>
This commit is contained in:
blake_bauman
2025-06-27 01:44:21 -07:00
committed by Josh Cummings
parent 686f8398dd
commit a4f813ab29
2 changed files with 101 additions and 1 deletions
@@ -3033,7 +3033,8 @@ public class ServerHttpSecurity {
/**
* Configures the logout handler. Default is
* {@code SecurityContextServerLogoutHandler}
* {@code SecurityContextServerLogoutHandler}. This clears any previous handlers
* configured.
* @param logoutHandler
* @return the {@link LogoutSpec} to configure
*/
@@ -3049,6 +3050,18 @@ public class ServerHttpSecurity {
return this;
}
/**
* Allows managing the list of {@link ServerLogoutHandler} instances.
* @param handlersConsumer {@link Consumer} for managing the list of handlers.
* @return the {@link LogoutSpec} to configure
* @since 7.0
*/
public LogoutSpec logoutHandler(Consumer<List<ServerLogoutHandler>> handlersConsumer) {
Assert.notNull(handlersConsumer, "consumer cannot be null");
handlersConsumer.accept(this.logoutHandlers);
return this;
}
/**
* Configures what URL a POST to will trigger a log out.
* @param logoutUrl the url to trigger a log out (i.e. "/signout" would mean a
@@ -16,18 +16,27 @@
package org.springframework.security.config.web.server;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriver;
import reactor.core.publisher.Mono;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.config.Customizer.withDefaults;
@@ -210,6 +219,84 @@ public class LogoutSpecTests {
FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class).assertAt();
}
@Test
public void multipleLogoutHandlers() {
InMemorySecurityContextRepository repository = new InMemorySecurityContextRepository();
MultiValueMap<String, String> logoutData = new LinkedMultiValueMap<>();
ServerLogoutHandler handler1 = (exchange, authentication) -> {
logoutData.add("handler-header", "value1");
return Mono.empty();
};
ServerLogoutHandler handler2 = (exchange, authentication) -> {
logoutData.add("handler-header", "value2");
return Mono.empty();
};
// @formatter:off
SecurityWebFilterChain securityWebFilter = this.http
.securityContextRepository(repository)
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated())
.formLogin(withDefaults())
.logout((logoutSpec) -> logoutSpec.logoutHandler((handlers) -> {
handlers.add(handler1);
handlers.add(0, handler2);
}))
.build();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(securityWebFilter)
.build();
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
.webTestClientSetup(webTestClient)
.build();
// @formatter:on
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage
.to(driver, FormLoginTests.DefaultLoginPage.class)
.assertAt();
// @formatter:off
loginPage = loginPage.loginForm()
.username("user")
.password("invalid")
.submit(FormLoginTests.DefaultLoginPage.class)
.assertError();
FormLoginTests.HomePage homePage = loginPage.loginForm()
.username("user")
.password("password")
.submit(FormLoginTests.HomePage.class);
// @formatter:on
homePage.assertAt();
SecurityContext savedContext = repository.getSavedContext();
assertThat(savedContext).isNotNull();
assertThat(savedContext.getAuthentication()).isInstanceOf(UsernamePasswordAuthenticationToken.class);
loginPage = FormLoginTests.DefaultLogoutPage.to(driver).assertAt().logout();
loginPage.assertAt().assertLogout();
assertThat(logoutData).hasSize(1);
assertThat(logoutData.get("handler-header")).containsExactly("value2", "value1");
savedContext = repository.getSavedContext();
assertThat(savedContext).isNull();
}
private static class InMemorySecurityContextRepository implements ServerSecurityContextRepository {
@Nullable private SecurityContext savedContext;
@Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
this.savedContext = context;
return Mono.empty();
}
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
return Mono.justOrEmpty(this.savedContext);
}
@Nullable private SecurityContext getSavedContext() {
return this.savedContext;
}
}
@RestController
public static class HomeController {