Skip to content

Commit a4f813a

Browse files
blake-baumanjzheaux
authored andcommitted
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 686f839 commit a4f813a

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)