Skip to content

Commit a8eb560

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 <blake_bauman@apple.com>
1 parent 3a014e3 commit a8eb560

File tree

2 files changed

+101
-7
lines changed

2 files changed

+101
-7
lines changed

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3026,7 +3026,7 @@ public final class LogoutSpec {
30263026

30273027
private final SecurityContextServerLogoutHandler DEFAULT_LOGOUT_HANDLER = new SecurityContextServerLogoutHandler();
30283028

3029-
private List<ServerLogoutHandler> logoutHandlers = new ArrayList<>();
3029+
private List<ServerLogoutHandler> logoutHandlers = new ArrayList<>(Arrays.asList(this.DEFAULT_LOGOUT_HANDLER));
30303030

30313031
private LogoutSpec() {
30323032
}
@@ -3044,14 +3044,21 @@ public LogoutSpec logoutHandler(ServerLogoutHandler logoutHandler) {
30443044
return addLogoutHandler(logoutHandler);
30453045
}
30463046

3047+
private LogoutSpec addLogoutHandler(ServerLogoutHandler logoutHandler) {
3048+
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
3049+
this.logoutHandlers.add(logoutHandler);
3050+
return this;
3051+
}
3052+
30473053
/**
3048-
* Adds a logout handler in the last position.
3049-
* @param logoutHandler
3054+
* Allows managing the list of {@link ServerLogoutHandler} instances.
3055+
* @param handlersConsumer {@link Consumer} for managing the list of handlers.
30503056
* @return the {@link LogoutSpec} to configure
3057+
* @since 7.0
30513058
*/
3052-
public LogoutSpec addLogoutHandler(ServerLogoutHandler logoutHandler) {
3053-
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
3054-
this.logoutHandlers.add(logoutHandler);
3059+
public LogoutSpec logoutHandler(Consumer<List<ServerLogoutHandler>> handlersConsumer) {
3060+
Assert.notNull(handlersConsumer, "consumer cannot be null");
3061+
handlersConsumer.accept(this.logoutHandlers);
30553062
return this;
30563063
}
30573064

@@ -3098,7 +3105,7 @@ private ServerLogoutHandler createLogoutHandler() {
30983105
this.DEFAULT_LOGOUT_HANDLER.setSecurityContextRepository(securityContextRepository);
30993106
}
31003107
if (this.logoutHandlers.isEmpty()) {
3101-
return DEFAULT_LOGOUT_HANDLER;
3108+
return this.DEFAULT_LOGOUT_HANDLER;
31023109
}
31033110
if (this.logoutHandlers.size() == 1) {
31043111
return this.logoutHandlers.get(0);

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)