Skip to content

Commit a45f40b

Browse files
committed
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 <[email protected]>
1 parent 910df47 commit a45f40b

File tree

2 files changed

+101
-1
lines changed

2 files changed

+101
-1
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3033,7 +3033,8 @@ private LogoutSpec() {
30333033

30343034
/**
30353035
* Configures the logout handler. Default is
3036-
* {@code SecurityContextServerLogoutHandler}
3036+
* {@code SecurityContextServerLogoutHandler}. This clears any previous handlers
3037+
* configured.
30373038
* @param logoutHandler
30383039
* @return the {@link LogoutSpec} to configure
30393040
*/
@@ -3049,6 +3050,18 @@ private LogoutSpec addLogoutHandler(ServerLogoutHandler logoutHandler) {
30493050
return this;
30503051
}
30513052

3053+
/**
3054+
* Allows managing the list of {@link ServerLogoutHandler} instances.
3055+
* @param handlersConsumer {@link Consumer} for managing the list of handlers.
3056+
* @return the {@link LogoutSpec} to configure
3057+
* @since 7.0
3058+
*/
3059+
public LogoutSpec logoutHandler(Consumer<List<ServerLogoutHandler>> handlersConsumer) {
3060+
Assert.notNull(handlersConsumer, "consumer cannot be null");
3061+
handlersConsumer.accept(this.logoutHandlers);
3062+
return this;
3063+
}
3064+
30523065
/**
30533066
* Configures what URL a POST to will trigger a log out.
30543067
* @param logoutUrl the url to trigger a log out (i.e. "/signout" would mean a

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,27 @@
1616

1717
package org.springframework.security.config.web.server;
1818

19+
import org.jspecify.annotations.Nullable;
1920
import org.junit.jupiter.api.Test;
2021
import org.openqa.selenium.WebDriver;
22+
import reactor.core.publisher.Mono;
2123

24+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
2225
import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder;
26+
import org.springframework.security.core.context.SecurityContext;
2327
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
2428
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
2529
import org.springframework.security.web.server.SecurityWebFilterChain;
30+
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
31+
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
2632
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
2733
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
2834
import org.springframework.test.web.reactive.server.WebTestClient;
35+
import org.springframework.util.LinkedMultiValueMap;
36+
import org.springframework.util.MultiValueMap;
2937
import org.springframework.web.bind.annotation.GetMapping;
3038
import org.springframework.web.bind.annotation.RestController;
39+
import org.springframework.web.server.ServerWebExchange;
3140

3241
import static org.assertj.core.api.Assertions.assertThat;
3342
import static org.springframework.security.config.Customizer.withDefaults;
@@ -210,6 +219,84 @@ public void logoutWhenCustomSecurityContextRepositoryThenLogsOut() {
210219
FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class).assertAt();
211220
}
212221

222+
@Test
223+
public void multipleLogoutHandlers() {
224+
InMemorySecurityContextRepository repository = new InMemorySecurityContextRepository();
225+
MultiValueMap<String, String> logoutData = new LinkedMultiValueMap<>();
226+
ServerLogoutHandler handler1 = (exchange, authentication) -> {
227+
logoutData.add("handler-header", "value1");
228+
return Mono.empty();
229+
};
230+
ServerLogoutHandler handler2 = (exchange, authentication) -> {
231+
logoutData.add("handler-header", "value2");
232+
return Mono.empty();
233+
};
234+
// @formatter:off
235+
SecurityWebFilterChain securityWebFilter = this.http
236+
.securityContextRepository(repository)
237+
.authorizeExchange((authorize) -> authorize
238+
.anyExchange().authenticated())
239+
.formLogin(withDefaults())
240+
.logout((logoutSpec) -> logoutSpec.logoutHandler((handlers) -> {
241+
handlers.add(handler1);
242+
handlers.add(0, handler2);
243+
}))
244+
.build();
245+
WebTestClient webTestClient = WebTestClientBuilder
246+
.bindToWebFilters(securityWebFilter)
247+
.build();
248+
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
249+
.webTestClientSetup(webTestClient)
250+
.build();
251+
// @formatter:on
252+
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage
253+
.to(driver, FormLoginTests.DefaultLoginPage.class)
254+
.assertAt();
255+
// @formatter:off
256+
loginPage = loginPage.loginForm()
257+
.username("user")
258+
.password("invalid")
259+
.submit(FormLoginTests.DefaultLoginPage.class)
260+
.assertError();
261+
FormLoginTests.HomePage homePage = loginPage.loginForm()
262+
.username("user")
263+
.password("password")
264+
.submit(FormLoginTests.HomePage.class);
265+
// @formatter:on
266+
homePage.assertAt();
267+
SecurityContext savedContext = repository.getSavedContext();
268+
assertThat(savedContext).isNotNull();
269+
assertThat(savedContext.getAuthentication()).isInstanceOf(UsernamePasswordAuthenticationToken.class);
270+
271+
loginPage = FormLoginTests.DefaultLogoutPage.to(driver).assertAt().logout();
272+
loginPage.assertAt().assertLogout();
273+
assertThat(logoutData).hasSize(1);
274+
assertThat(logoutData.get("handler-header")).containsExactly("value2", "value1");
275+
savedContext = repository.getSavedContext();
276+
assertThat(savedContext).isNull();
277+
}
278+
279+
private static class InMemorySecurityContextRepository implements ServerSecurityContextRepository {
280+
281+
@Nullable private SecurityContext savedContext;
282+
283+
@Override
284+
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
285+
this.savedContext = context;
286+
return Mono.empty();
287+
}
288+
289+
@Override
290+
public Mono<SecurityContext> load(ServerWebExchange exchange) {
291+
return Mono.justOrEmpty(this.savedContext);
292+
}
293+
294+
@Nullable private SecurityContext getSavedContext() {
295+
return this.savedContext;
296+
}
297+
298+
}
299+
213300
@RestController
214301
public static class HomeController {
215302

0 commit comments

Comments
 (0)