|
@@ -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 {
|
|
|
|