فهرست منبع

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>
blake_bauman 2 ماه پیش
والد
کامیت
a4f813ab29

+ 14 - 1
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

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

+ 87 - 0
config/src/test/java/org/springframework/security/config/web/server/LogoutSpecTests.java

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