Преглед изворни кода

Add Modular Spring Security Configuration

Closes gh-16258
Rob Winch пре 2 дана
родитељ
комит
a8f045eb50
49 измењених фајлова са 3207 додато и 0 уклоњено
  1. 52 0
      config/src/main/java/org/springframework/security/config/ThrowingCustomizer.java
  2. 75 0
      config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java
  3. 82 0
      config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java
  4. 4 0
      config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
  5. 119 0
      config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt
  6. 119 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt
  7. 209 0
      config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java
  8. 107 0
      config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java
  9. 33 0
      config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt
  10. 69 0
      docs/modules/ROOT/pages/reactive/configuration/webflux.adoc
  11. 69 0
      docs/modules/ROOT/pages/servlet/configuration/java.adoc
  12. 73 0
      docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc
  13. 1 0
      docs/modules/ROOT/pages/whats-new.adoc
  14. 99 0
      docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java
  15. 76 0
      docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java
  16. 61 0
      docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.java
  17. 56 0
      docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.java
  18. 58 0
      docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java
  19. 61 0
      docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java
  20. 102 0
      docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java
  21. 64 0
      docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java
  22. 77 0
      docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.java
  23. 74 0
      docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.java
  24. 71 0
      docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java
  25. 66 0
      docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java
  26. 104 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt
  27. 74 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt
  28. 99 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt
  29. 72 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingTests.kt
  30. 44 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.kt
  31. 37 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.kt
  32. 42 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanConfiguration.kt
  33. 38 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanTests.kt
  34. 43 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt
  35. 35 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt
  36. 36 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt
  37. 37 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanTests.kt
  38. 104 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt
  39. 72 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt
  40. 110 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt
  41. 70 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingTests.kt
  42. 46 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.kt
  43. 35 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.kt
  44. 52 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanConfiguration.kt
  45. 36 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanTests.kt
  46. 42 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt
  47. 31 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt
  48. 40 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt
  49. 31 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanTests.kt

+ 52 - 0
config/src/main/java/org/springframework/security/config/ThrowingCustomizer.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config;
+
+/**
+ * A {@link Customizer} that allows invocation of code that throws a checked exception.
+ *
+ * @param <T> The type of input.
+ */
+@FunctionalInterface
+public interface ThrowingCustomizer<T> extends Customizer<T> {
+
+	/**
+	 * Default {@link Customizer#customize(Object)} that wraps any thrown checked
+	 * exceptions (by default in a {@link RuntimeException}).
+	 * @param t the object to customize
+	 */
+	default void customize(T t) {
+		try {
+			customizeWithException(t);
+		}
+		catch (RuntimeException ex) {
+			throw ex;
+		}
+		catch (Exception ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
+	/**
+	 * Performs the customization on the given object, possibly throwing a checked
+	 * exception.
+	 * @param t the object to customize
+	 * @throws Exception on error
+	 */
+	void customizeWithException(T t) throws Exception;
+
+}

+ 75 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java

@@ -16,19 +16,24 @@
 
 
 package org.springframework.security.config.annotation.web.configuration;
 package org.springframework.security.config.annotation.web.configuration;
 
 
+import java.lang.reflect.Modifier;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
+import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Scope;
 import org.springframework.context.annotation.Scope;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ResolvableType;
 import org.springframework.core.io.support.SpringFactoriesLoader;
 import org.springframework.core.io.support.SpringFactoriesLoader;
 import org.springframework.security.authentication.AuthenticationEventPublisher;
 import org.springframework.security.authentication.AuthenticationEventPublisher;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
 import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.ObjectPostProcessor;
 import org.springframework.security.config.ObjectPostProcessor;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
 import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@@ -46,6 +51,7 @@ import org.springframework.security.crypto.factory.PasswordEncoderFactories;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
 import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.util.ReflectionUtils;
 import org.springframework.util.function.ThrowingSupplier;
 import org.springframework.util.function.ThrowingSupplier;
 import org.springframework.web.accept.ContentNegotiationStrategy;
 import org.springframework.web.accept.ContentNegotiationStrategy;
 import org.springframework.web.accept.HeaderContentNegotiationStrategy;
 import org.springframework.web.accept.HeaderContentNegotiationStrategy;
@@ -131,6 +137,8 @@ class HttpSecurityConfiguration {
 		// @formatter:on
 		// @formatter:on
 		applyCorsIfAvailable(http);
 		applyCorsIfAvailable(http);
 		applyDefaultConfigurers(http);
 		applyDefaultConfigurers(http);
+		applyHttpSecurityCustomizers(this.context, http);
+		applyTopLevelCustomizers(this.context, http);
 		return http;
 		return http;
 	}
 	}
 
 
@@ -160,6 +168,73 @@ class HttpSecurityConfiguration {
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Applies all {@code Customizer<HttpSecurity>} Bean instances to the
+	 * {@link HttpSecurity} instance.
+	 * @param applicationContext the {@link ApplicationContext} to lookup Bean instances
+	 * @param http the {@link HttpSecurity} to apply the Beans to.
+	 */
+	private void applyHttpSecurityCustomizers(ApplicationContext applicationContext, HttpSecurity http) {
+		ResolvableType httpSecurityCustomizerType = ResolvableType.forClassWithGenerics(Customizer.class,
+				HttpSecurity.class);
+		ObjectProvider<Customizer<HttpSecurity>> customizerProvider = this.context
+			.getBeanProvider(httpSecurityCustomizerType);
+
+		// @formatter:off
+		customizerProvider.orderedStream().forEach((customizer) ->
+				customizer.customize(http)
+		);
+		// @formatter:on
+	}
+
+	/**
+	 * Applies all {@link Customizer} Beans to {@link HttpSecurity}. For each public,
+	 * non-static method in HttpSecurity that accepts a Customizer
+	 * <ul>
+	 * <li>Use the {@link MethodParameter} (this preserves generics) to resolve all Beans
+	 * for that type</li>
+	 * <li>For each {@link Customizer} Bean invoke the {@link java.lang.reflect.Method}
+	 * with the {@link Customizer} Bean as the argument</li>
+	 * </ul>
+	 * @param context the {@link ApplicationContext}
+	 * @param http the {@link HttpSecurity}
+	 * @throws Exception
+	 */
+	private void applyTopLevelCustomizers(ApplicationContext context, HttpSecurity http) {
+		ReflectionUtils.MethodFilter isCustomizerMethod = (method) -> {
+			if (Modifier.isStatic(method.getModifiers())) {
+				return false;
+			}
+			if (!Modifier.isPublic(method.getModifiers())) {
+				return false;
+			}
+			if (!method.canAccess(http)) {
+				return false;
+			}
+			if (method.getParameterCount() != 1) {
+				return false;
+			}
+			if (method.getParameterTypes()[0] == Customizer.class) {
+				return true;
+			}
+			return false;
+		};
+		ReflectionUtils.MethodCallback invokeWithEachCustomizerBean = (customizerMethod) -> {
+
+			MethodParameter customizerParameter = new MethodParameter(customizerMethod, 0);
+			ResolvableType customizerType = ResolvableType.forMethodParameter(customizerParameter);
+			ObjectProvider<?> customizerProvider = context.getBeanProvider(customizerType);
+
+			// @formatter:off
+			customizerProvider.orderedStream().forEach((customizer) ->
+				ReflectionUtils.invokeMethod(customizerMethod, http, customizer)
+			);
+			// @formatter:on
+
+		};
+		ReflectionUtils.doWithMethods(HttpSecurity.class, invokeWithEachCustomizerBean, isCustomizerMethod);
+	}
+
 	private Map<Class<?>, Object> createSharedObjects() {
 	private Map<Class<?>, Object> createSharedObjects() {
 		Map<Class<?>, Object> sharedObjects = new HashMap<>();
 		Map<Class<?>, Object> sharedObjects = new HashMap<>();
 		sharedObjects.put(ApplicationContext.class, this.context);
 		sharedObjects.put(ApplicationContext.class, this.context);

+ 82 - 0
config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java

@@ -16,6 +16,7 @@
 
 
 package org.springframework.security.config.annotation.web.reactive;
 package org.springframework.security.config.annotation.web.reactive;
 
 
+import java.lang.reflect.Modifier;
 import java.util.Map;
 import java.util.Map;
 
 
 import org.springframework.beans.BeansException;
 import org.springframework.beans.BeansException;
@@ -28,10 +29,13 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Scope;
 import org.springframework.context.annotation.Scope;
 import org.springframework.context.expression.BeanFactoryResolver;
 import org.springframework.context.expression.BeanFactoryResolver;
+import org.springframework.core.MethodParameter;
 import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.core.ReactiveAdapterRegistry;
+import org.springframework.core.ResolvableType;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
 import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
 import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
 import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.ObjectPostProcessor;
 import org.springframework.security.config.ObjectPostProcessor;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
 import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
@@ -40,6 +44,7 @@ import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver;
 import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver;
 import org.springframework.security.web.reactive.result.method.annotation.CurrentSecurityContextArgumentResolver;
 import org.springframework.security.web.reactive.result.method.annotation.CurrentSecurityContextArgumentResolver;
+import org.springframework.util.ReflectionUtils;
 import org.springframework.web.reactive.config.WebFluxConfigurer;
 import org.springframework.web.reactive.config.WebFluxConfigurer;
 import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
 import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
 
 
@@ -154,6 +159,83 @@ class ServerHttpSecurityConfiguration {
 
 
 	@Bean(HTTPSECURITY_BEAN_NAME)
 	@Bean(HTTPSECURITY_BEAN_NAME)
 	@Scope("prototype")
 	@Scope("prototype")
+	ServerHttpSecurity httpSecurity(ApplicationContext context) {
+		ServerHttpSecurity http = httpSecurity();
+		applyServerHttpSecurityCustomizers(context, http);
+		applyTopLevelBeanCustomizers(context, http);
+		return http;
+	}
+
+	/**
+	 * Applies all {@code Custmizer<ServerHttpSecurity>} Beans to
+	 * {@link ServerHttpSecurity}.
+	 * @param context the {@link ApplicationContext}
+	 * @param http the {@link ServerHttpSecurity}
+	 * @throws Exception
+	 */
+	private void applyServerHttpSecurityCustomizers(ApplicationContext context, ServerHttpSecurity http) {
+		ResolvableType httpSecurityCustomizerType = ResolvableType.forClassWithGenerics(Customizer.class,
+				ServerHttpSecurity.class);
+		ObjectProvider<Customizer<ServerHttpSecurity>> customizerProvider = context
+			.getBeanProvider(httpSecurityCustomizerType);
+
+		// @formatter:off
+		customizerProvider.orderedStream().forEach((customizer) ->
+			customizer.customize(http)
+		);
+		// @formatter:on
+	}
+
+	/**
+	 * Applies all {@link Customizer} Beans to top level {@link ServerHttpSecurity}
+	 * method.
+	 *
+	 * For each public, non-static method in ServerHttpSecurity that accepts a Customizer
+	 * <ul>
+	 * <li>Use the {@link MethodParameter} (this preserves generics) to resolve all Beans
+	 * for that type</li>
+	 * <li>For each {@link Customizer} Bean invoke the {@link java.lang.reflect.Method}
+	 * with the {@link Customizer} Bean as the argument</li>
+	 * </ul>
+	 * @param context the {@link ApplicationContext}
+	 * @param http the {@link ServerHttpSecurity}
+	 * @throws Exception
+	 */
+	private void applyTopLevelBeanCustomizers(ApplicationContext context, ServerHttpSecurity http) {
+		ReflectionUtils.MethodFilter isCustomizerMethod = (method) -> {
+			if (Modifier.isStatic(method.getModifiers())) {
+				return false;
+			}
+			if (!Modifier.isPublic(method.getModifiers())) {
+				return false;
+			}
+			if (!method.canAccess(http)) {
+				return false;
+			}
+			if (method.getParameterCount() != 1) {
+				return false;
+			}
+			if (method.getParameterTypes()[0] == Customizer.class) {
+				return true;
+			}
+			return false;
+		};
+		ReflectionUtils.MethodCallback invokeWithEachCustomizerBean = (customizerMethod) -> {
+
+			MethodParameter customizerParameter = new MethodParameter(customizerMethod, 0);
+			ResolvableType customizerType = ResolvableType.forMethodParameter(customizerParameter);
+			ObjectProvider<?> customizerProvider = context.getBeanProvider(customizerType);
+
+			// @formatter:off
+			customizerProvider.orderedStream().forEach((customizer) ->
+					ReflectionUtils.invokeMethod(customizerMethod, http, customizer)
+			);
+			// @formatter:on
+
+		};
+		ReflectionUtils.doWithMethods(ServerHttpSecurity.class, invokeWithEachCustomizerBean, isCustomizerMethod);
+	}
+
 	ServerHttpSecurity httpSecurity() {
 	ServerHttpSecurity httpSecurity() {
 		ContextAwareServerHttpSecurity http = new ContextAwareServerHttpSecurity();
 		ContextAwareServerHttpSecurity http = new ContextAwareServerHttpSecurity();
 		// @formatter:off
 		// @formatter:off

+ 4 - 0
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -1316,6 +1316,10 @@ public class ServerHttpSecurity {
 		return this.context.getBeanNamesForType(beanClass);
 		return this.context.getBeanNamesForType(beanClass);
 	}
 	}
 
 
+	ApplicationContext getApplicationContext() {
+		return this.context;
+	}
+
 	protected void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 	protected void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 		this.context = applicationContext;
 		this.context = applicationContext;
 	}
 	}

+ 119 - 0
config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt

@@ -18,14 +18,22 @@ package org.springframework.security.config.annotation.web
 
 
 import jakarta.servlet.Filter
 import jakarta.servlet.Filter
 import jakarta.servlet.http.HttpServletRequest
 import jakarta.servlet.http.HttpServletRequest
+import org.springframework.beans.factory.ObjectProvider
 import org.springframework.context.ApplicationContext
 import org.springframework.context.ApplicationContext
+import org.springframework.core.MethodParameter
+import org.springframework.core.ResolvableType
+import org.springframework.core.annotation.AnnotationUtils
 import org.springframework.security.authentication.AuthenticationManager
 import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.config.Customizer
 import org.springframework.security.config.annotation.SecurityConfigurerAdapter
 import org.springframework.security.config.annotation.SecurityConfigurerAdapter
 import org.springframework.security.config.annotation.web.builders.HttpSecurity
 import org.springframework.security.config.annotation.web.builders.HttpSecurity
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
 import org.springframework.security.web.DefaultSecurityFilterChain
 import org.springframework.security.web.DefaultSecurityFilterChain
 import org.springframework.security.web.util.matcher.RequestMatcher
 import org.springframework.security.web.util.matcher.RequestMatcher
+import org.springframework.util.ReflectionUtils
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
 
 
 /**
 /**
  * Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl].
  * Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl].
@@ -77,6 +85,117 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
     var authenticationManager: AuthenticationManager? = null
     var authenticationManager: AuthenticationManager? = null
     val context: ApplicationContext = http.getSharedObject(ApplicationContext::class.java)
     val context: ApplicationContext = http.getSharedObject(ApplicationContext::class.java)
 
 
+    init {
+        applyFunction1HttpSecurityDslBeans(this.context, this)
+        applyTopLevelFunction1SecurityDslBeans(this.context, this)
+    }
+
+
+    /**
+     * Applies all `Function1<HttpSecurity,Unit>` Beans which
+     * allows exposing the DSL as Beans to be applied.
+     *
+     * ```
+     * @Bean
+     * fun httpSecurityDslBean(): HttpSecurityDsl.() -> Unit {
+     *     return {
+     *         headers {
+     *             contentSecurityPolicy {
+     *                 policyDirectives = "object-src 'none'"
+     *             }
+     *         }
+     *         redirectToHttps { }
+     *    }
+     * }
+     * ```
+     */
+    private fun applyFunction1HttpSecurityDslBeans(context: ApplicationContext, http: HttpSecurityDsl) : Unit {
+        val httpSecurityDslFnType = ResolvableType.forClassWithGenerics(Function1::class.java,
+            HttpSecurityDsl::class.java, Unit::class.java)
+        val httpSecurityDslFnProvider = context
+            .getBeanProvider<Function1<HttpSecurityDsl,Unit>>(httpSecurityDslFnType)
+
+        // @formatter:off
+        httpSecurityDslFnProvider.orderedStream().forEach { fn -> fn.invoke(http) }
+        // @formatter:on
+    }
+
+    /**
+     * Applies all `Function1<T,Unit>` Beans such that `T` is a top level
+     * DSL on `HttpSecurityDsl`. This allows exposing the top level
+     * DSLs as Beans to be applied.
+     *
+     *
+     * ```
+     * @Bean
+     * fun httpSecurityCustomizer(): ThrowingCustomizer<HttpSecurity> {
+     *     return ThrowingCustomizer { http -> http
+     *         .headers { headers -> headers
+     *             .contentSecurityPolicy { csp -> csp
+     *                 .policyDirectives("object-src 'none'")
+     *             }
+     *         }
+     *         .redirectToHttps(Customizer.withDefaults())
+     *     }
+     * }
+     * ```
+     *
+     * @param context the [ApplicationContext]
+     * @param http the [HttpSecurity]
+     * @throws Exception
+     */
+    private fun applyTopLevelFunction1SecurityDslBeans(context: ApplicationContext, http: HttpSecurityDsl) {
+        val isCustomizerMethod = ReflectionUtils.MethodFilter { method: Method ->
+            if (Modifier.isStatic(method.modifiers)) {
+                return@MethodFilter false
+            }
+            if (!Modifier.isPublic(method.modifiers)) {
+                return@MethodFilter false
+            }
+            if (!method.canAccess(http)) {
+                return@MethodFilter false
+            }
+            if (method.parameterCount != 1) {
+                return@MethodFilter false
+            }
+            return@MethodFilter extractDslType(method) != null
+        }
+        val invokeWithEachDslBean = ReflectionUtils.MethodCallback { dslMethod: Method ->
+            val dslFunctionType = firstMethodResolvableType(dslMethod)!!
+            val dslFunctionProvider: ObjectProvider<*> = context.getBeanProvider<Any>(dslFunctionType)
+
+            // @formatter:off
+            dslFunctionProvider.orderedStream().forEach {customizer: Any -> ReflectionUtils.invokeMethod(dslMethod, http, customizer)}
+        }
+        ReflectionUtils.doWithMethods(HttpSecurityDsl::class.java, invokeWithEachDslBean, isCustomizerMethod)
+    }
+
+    /**
+     * From a `Method` with the first argument `Function<T,Unit>` return `T` or `null`
+     * if the first argument is not a `Function`.
+     * @return From a `Method` with the first argument `Function<T,Unit>` return `T`.
+     */
+    private fun extractDslType(method: Method): ResolvableType? {
+        val functionType = firstMethodResolvableType(method)
+        if (!Function::class.java.isAssignableFrom(functionType.toClass())) {
+            return null
+        }
+        val functionInputType = functionType.getGeneric(0)
+        val securityMarker = AnnotationUtils.findAnnotation(functionInputType.toClass(), SecurityMarker::class.java)
+        val isSecurityDsl = securityMarker != null
+        if (!isSecurityDsl) {
+            return null
+        }
+        return functionInputType
+    }
+
+    private fun firstMethodResolvableType(method: Method): ResolvableType {
+        val parameter = MethodParameter(
+            method, 0
+        )
+        return ResolvableType.forMethodParameter(parameter)
+    }
+
     /**
     /**
      * Applies a [SecurityConfigurerAdapter] to this [HttpSecurity]
      * Applies a [SecurityConfigurerAdapter] to this [HttpSecurity]
      *
      *

+ 119 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt

@@ -16,13 +16,23 @@
 
 
 package org.springframework.security.config.web.server
 package org.springframework.security.config.web.server
 
 
+import org.springframework.beans.factory.ObjectProvider
+import org.springframework.context.ApplicationContext
+import org.springframework.core.MethodParameter
+import org.springframework.core.ResolvableType
+import org.springframework.core.annotation.AnnotationUtils
 import org.springframework.security.authentication.ReactiveAuthenticationManager
 import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
 import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
 import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
 import org.springframework.security.web.server.SecurityWebFilterChain
 import org.springframework.security.web.server.SecurityWebFilterChain
 import org.springframework.security.web.server.context.ServerSecurityContextRepository
 import org.springframework.security.web.server.context.ServerSecurityContextRepository
 import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
 import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
+import org.springframework.util.ReflectionUtils
 import org.springframework.web.server.ServerWebExchange
 import org.springframework.web.server.ServerWebExchange
 import org.springframework.web.server.WebFilter
 import org.springframework.web.server.WebFilter
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
 
 
 /**
 /**
  * Configures [ServerHttpSecurity] using a [ServerHttpSecurity Kotlin DSL][ServerHttpSecurityDsl].
  * Configures [ServerHttpSecurity] using a [ServerHttpSecurity Kotlin DSL][ServerHttpSecurityDsl].
@@ -68,6 +78,115 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
     var authenticationManager: ReactiveAuthenticationManager? = null
     var authenticationManager: ReactiveAuthenticationManager? = null
     var securityContextRepository: ServerSecurityContextRepository? = null
     var securityContextRepository: ServerSecurityContextRepository? = null
 
 
+    init {
+        applyFunction1HttpSecurityDslBeans(this.http.applicationContext, this)
+        applyTopLevelFunction1SecurityDslBeans(this.http.applicationContext, this)
+    }
+
+    /**
+     * Applies all `Function1<ServerHttpSecurityDsl,Unit>` Beans which
+     * allows exposing the DSL as Beans to be applied.
+     *
+     * ```
+     * @Bean
+     * @Order(Ordered.LOWEST_PRECEDENCE)
+     * fun userAuthorization(): ServerHttpSecurityDsl.() -> Unit {
+     *     // @formatter:off
+     *     return {
+     *         authorizeExchange {
+     *             authorize("/user/profile", hasRole("USER"))
+     *         }
+     *     }
+     *     // @formatter:on
+     * }
+     * ```
+     */
+    private fun applyFunction1HttpSecurityDslBeans(context: ApplicationContext, http: ServerHttpSecurityDsl) : Unit {
+        val httpSecurityDslFnType = ResolvableType.forClassWithGenerics(Function1::class.java,
+            ServerHttpSecurityDsl::class.java, Unit::class.java)
+        val httpSecurityDslFnProvider = context
+            .getBeanProvider<Function1<ServerHttpSecurityDsl,Unit>>(httpSecurityDslFnType)
+
+        // @formatter:off
+        httpSecurityDslFnProvider.orderedStream().forEach { fn -> fn.invoke(http) }
+        // @formatter:on
+    }
+
+    /**
+     * Applies all `Function1<T,Unit>` Beans such that `T` is a top level
+     * DSL on `ServerHttpSecurityDsl`. This allows exposing the top level
+     * DSLs as Beans to be applied.
+     *
+     * ```
+     * @Bean
+     * fun headersSecurity(): Customizer<ServerHttpSecurity.HeaderSpec> {
+     *     // @formatter:off
+     *     return Customizer { headers -> headers
+     *         .contentSecurityPolicy { csp -> csp
+     *             .policyDirectives("object-src 'none'")
+     *         }
+     *     }
+     *     // @formatter:on
+     * }
+     * ```
+     *
+     * @param context the [ApplicationContext]
+     * @param http the [HttpSecurity]
+     * @throws Exception
+     */
+    private fun applyTopLevelFunction1SecurityDslBeans(context: ApplicationContext, http: ServerHttpSecurityDsl) {
+        val isCustomizerMethod = ReflectionUtils.MethodFilter { method: Method ->
+            if (Modifier.isStatic(method.modifiers)) {
+                return@MethodFilter false
+            }
+            if (!Modifier.isPublic(method.modifiers)) {
+                return@MethodFilter false
+            }
+            if (!method.canAccess(http)) {
+                return@MethodFilter false
+            }
+            if (method.parameterCount != 1) {
+                return@MethodFilter false
+            }
+            return@MethodFilter extractDslType(method) != null
+        }
+
+        val invokeWithEachDslBean = ReflectionUtils.MethodCallback { dslMethod: Method ->
+            val dslFunctionType = firstMethodResolvableType(dslMethod)!!
+            val dslFunctionProvider: ObjectProvider<*> = context.getBeanProvider<Any>(dslFunctionType)
+
+            // @formatter:off
+            dslFunctionProvider.orderedStream().forEach {customizer: Any -> ReflectionUtils.invokeMethod(dslMethod, http, customizer)}
+        }
+        ReflectionUtils.doWithMethods(ServerHttpSecurityDsl::class.java, invokeWithEachDslBean, isCustomizerMethod)
+    }
+
+    /**
+     * From a `Method` with the first argument `Function<T,Unit>` return `T` or `null`
+     * if the first argument is not a `Function`.
+     * @return From a `Method` with the first argument `Function<T,Unit>` return `T`.
+     */
+    private fun extractDslType(method: Method): ResolvableType? {
+        val functionType = firstMethodResolvableType(method)
+        if (!Function::class.java.isAssignableFrom(functionType.toClass())) {
+            return null
+        }
+        val functionInputType = functionType.getGeneric(0)
+        val securityMarker = AnnotationUtils.findAnnotation(functionInputType.toClass(), ServerSecurityMarker::class.java)
+        val isSecurityDsl = securityMarker != null
+        if (!isSecurityDsl) {
+            return null
+        }
+        return functionInputType
+    }
+
+    private fun firstMethodResolvableType(method: Method): ResolvableType {
+        val parameter = MethodParameter(
+            method, 0
+        )
+        return ResolvableType.forMethodParameter(parameter)
+    }
+
     /**
     /**
      * Allows configuring the [ServerHttpSecurity] to only be invoked when matching the
      * Allows configuring the [ServerHttpSecurity] to only be invoked when matching the
      * provided [ServerWebExchangeMatcher].
      * provided [ServerWebExchangeMatcher].

+ 209 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java

@@ -28,14 +28,20 @@ import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletResponse;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.Mock;
 import org.mockito.MockedStatic;
 import org.mockito.MockedStatic;
+import org.mockito.Mockito;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
 import org.springframework.context.event.EventListener;
 import org.springframework.context.event.EventListener;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
 import org.springframework.core.io.support.SpringFactoriesLoader;
 import org.springframework.core.io.support.SpringFactoriesLoader;
 import org.springframework.mock.web.MockHttpSession;
 import org.springframework.mock.web.MockHttpSession;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.AccessDeniedException;
@@ -54,6 +60,7 @@ import org.springframework.security.config.annotation.SecurityContextChangedList
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer;
 import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer;
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
 import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
 import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.config.test.SpringTestContextExtension;
@@ -82,12 +89,15 @@ import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.cors.CorsConfigurationSource;
 import org.springframework.web.cors.CorsConfigurationSource;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 import org.springframework.web.filter.CorsFilter;
 import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.willAnswer;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.withSettings;
 import static org.springframework.security.config.Customizer.withDefaults;
 import static org.springframework.security.config.Customizer.withDefaults;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
@@ -425,6 +435,77 @@ public class HttpSecurityConfigurationTests {
 			.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
 			.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
 	}
 	}
 
 
+	@Test
+	void authorizeHttpRequestsCustomizerBean() throws Exception {
+		this.spring.register(AuthorizeRequestsBeanConfiguration.class, UserDetailsConfig.class).autowire();
+
+		Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeRequests = this.spring
+			.getContext()
+			.getBean("authorizeRequests", Customizer.class);
+
+		verify(authorizeRequests).customize(any());
+
+	}
+
+	@Test
+	void multiAuthorizeHttpRequestsCustomizerBean() throws Exception {
+		this.spring.register(MultiAuthorizeRequestsBeanConfiguration.class, UserDetailsConfig.class).autowire();
+
+		Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeRequests0 = this.spring
+			.getContext()
+			.getBean("authorizeRequests0", Customizer.class);
+		Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeRequests = this.spring
+			.getContext()
+			.getBean("authorizeRequests", Customizer.class);
+		InOrder inOrder = Mockito.inOrder(authorizeRequests0, authorizeRequests);
+
+		ArgumentCaptor<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> arg0 = ArgumentCaptor
+			.forClass(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry.class);
+		ArgumentCaptor<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> arg1 = ArgumentCaptor
+			.forClass(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry.class);
+		inOrder.verify(authorizeRequests0).customize(arg0.capture());
+		inOrder.verify(authorizeRequests).customize(arg1.capture());
+	}
+
+	@Test
+	void disableAuthorizeHttpRequestsCustomizerBean() throws Exception {
+		this.spring.register(AuthorizeRequestsBeanConfiguration.class, UserDetailsConfig.class).autowire();
+
+		Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeRequests = this.spring
+			.getContext()
+			.getBean("authorizeRequests", Customizer.class);
+
+		verify(authorizeRequests).customize(any());
+
+	}
+
+	@Test
+	void httpSecurityCustomizerBean() throws Exception {
+		this.spring.register(HttpSecurityCustomizerBeanConfiguration.class, UserDetailsConfig.class).autowire();
+
+		Customizer<HttpSecurity> httpSecurityCustomizer = this.spring.getContext()
+			.getBean("httpSecurityCustomizer", Customizer.class);
+
+		ArgumentCaptor<HttpSecurity> arg0 = ArgumentCaptor.forClass(HttpSecurity.class);
+		verify(httpSecurityCustomizer).customize(arg0.capture());
+	}
+
+	@Test
+	void multiHttpSecurityCustomizerBean() throws Exception {
+		this.spring.register(MultiHttpSecurityCustomizerBeanConfiguration.class, UserDetailsConfig.class).autowire();
+
+		Customizer<HttpSecurity> httpSecurityCustomizer = this.spring.getContext()
+			.getBean("httpSecurityCustomizer", Customizer.class);
+		Customizer<HttpSecurity> httpSecurityCustomizer0 = this.spring.getContext()
+			.getBean("httpSecurityCustomizer0", Customizer.class);
+		InOrder inOrder = Mockito.inOrder(httpSecurityCustomizer0, httpSecurityCustomizer);
+
+		ArgumentCaptor<HttpSecurity> arg0 = ArgumentCaptor.forClass(HttpSecurity.class);
+		ArgumentCaptor<HttpSecurity> arg1 = ArgumentCaptor.forClass(HttpSecurity.class);
+		inOrder.verify(httpSecurityCustomizer0).customize(arg0.capture());
+		inOrder.verify(httpSecurityCustomizer).customize(arg1.capture());
+	}
+
 	@RestController
 	@RestController
 	static class NameController {
 	static class NameController {
 
 
@@ -785,6 +866,134 @@ public class HttpSecurityConfigurationTests {
 
 
 	}
 	}
 
 
+	@Configuration(proxyBeanMethods = false)
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class AuthorizeRequestsBeanConfiguration {
+
+		@Bean
+		SecurityFilterChain noAuthorizeSecurity(HttpSecurity http) throws Exception {
+			http.httpBasic(withDefaults());
+			return http.build();
+		}
+
+		@Bean
+		static Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeRequests()
+				throws Exception {
+			Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authz = mock(
+					Customizer.class, withSettings().name("authz"));
+			// prevent validation errors of no authorization rules being defined
+			willAnswer(((invocation) -> {
+				AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry requests = invocation
+					.getArgument(0);
+				requests.anyRequest().authenticated();
+				return null;
+			})).given(authz).customize(any());
+			return authz;
+		}
+
+		@RestController
+		static class PublicController {
+
+			@GetMapping("/public")
+			String permitAll() {
+				return "public";
+			}
+
+		}
+
+	}
+
+	@Configuration(proxyBeanMethods = false)
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class DisableAuthorizeRequestsBeanConfiguration {
+
+		@Bean
+		SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
+			http.httpBasic(withDefaults());
+			// @formatter:off
+			http.authorizeHttpRequests((requests) -> requests
+				.anyRequest().permitAll()
+			);
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		static Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeRequests()
+				throws Exception {
+			// @formatter:off
+			return (requests) -> requests
+				.anyRequest().denyAll();
+			// @formatter:on
+		}
+
+		@RestController
+		static class PublicController {
+
+			@GetMapping("/public")
+			String permitAll() {
+				return "public";
+			}
+
+		}
+
+	}
+
+	@Configuration(proxyBeanMethods = false)
+	@Import(AuthorizeRequestsBeanConfiguration.class)
+	static class MultiAuthorizeRequestsBeanConfiguration {
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE)
+		static Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeRequests0()
+				throws Exception {
+			return mock(Customizer.class, withSettings().name("authz0"));
+		}
+
+	}
+
+	@Configuration(proxyBeanMethods = false)
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class HttpSecurityCustomizerBeanConfiguration {
+
+		@Bean
+		SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
+			http.httpBasic(withDefaults());
+			return http.build();
+		}
+
+		@Bean
+		static Customizer<HttpSecurity> httpSecurityCustomizer() {
+			return mock(Customizer.class, withSettings().name("httpSecurityCustomizer"));
+		}
+
+		@RestController
+		static class PublicController {
+
+			@GetMapping("/public")
+			String permitAll() {
+				return "public";
+			}
+
+		}
+
+	}
+
+	@Configuration(proxyBeanMethods = false)
+	@Import(HttpSecurityCustomizerBeanConfiguration.class)
+	static class MultiHttpSecurityCustomizerBeanConfiguration {
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE)
+		static Customizer<HttpSecurity> httpSecurityCustomizer0() throws Exception {
+			return mock(Customizer.class, withSettings().name("httpSecurityCustomizer0"));
+		}
+
+	}
+
 	private static class TestCompromisedPasswordChecker implements CompromisedPasswordChecker {
 	private static class TestCompromisedPasswordChecker implements CompromisedPasswordChecker {
 
 
 		@Override
 		@Override

+ 107 - 0
config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java

@@ -29,12 +29,17 @@ import io.micrometer.observation.ObservationRegistry;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
 import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
 import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.password.CompromisedPasswordDecision;
 import org.springframework.security.authentication.password.CompromisedPasswordDecision;
@@ -46,6 +51,7 @@ import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
 import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
 import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -267,6 +273,47 @@ public class ServerHttpSecurityConfigurationTests {
 		assertThat(contexts.next().getContextualName()).isEqualTo("security filterchain after");
 		assertThat(contexts.next().getContextualName()).isEqualTo("security filterchain after");
 	}
 	}
 
 
+	@Test
+	void authorizeExchangeCustomizerBean() {
+		this.spring.register(AuthorizeExchangeCustomizerBeanConfig.class).autowire();
+		Customizer<AuthorizeExchangeSpec> authzCustomizer = this.spring.getContext().getBean("authz", Customizer.class);
+
+		ArgumentCaptor<AuthorizeExchangeSpec> arg0 = ArgumentCaptor.forClass(AuthorizeExchangeSpec.class);
+		verify(authzCustomizer).customize(arg0.capture());
+	}
+
+	@Test
+	void multiAuthorizeExchangeCustomizerBean() {
+		this.spring.register(MultiAuthorizeExchangeCustomizerBeanConfig.class).autowire();
+		Customizer<AuthorizeExchangeSpec> authzCustomizer = this.spring.getContext().getBean("authz", Customizer.class);
+
+		ArgumentCaptor<AuthorizeExchangeSpec> arg0 = ArgumentCaptor.forClass(AuthorizeExchangeSpec.class);
+		verify(authzCustomizer).customize(arg0.capture());
+	}
+
+	@Test
+	void serverHttpSecurityCustomizerBean() {
+		this.spring.register(ServerHttpSecurityCustomizerConfig.class).autowire();
+		Customizer<ServerHttpSecurity> httpSecurityCustomizer = this.spring.getContext()
+			.getBean("httpSecurityCustomizer", Customizer.class);
+
+		ArgumentCaptor<ServerHttpSecurity> arg0 = ArgumentCaptor.forClass(ServerHttpSecurity.class);
+		verify(httpSecurityCustomizer).customize(arg0.capture());
+	}
+
+	@Test
+	void multiServerHttpSecurityCustomizerBean() {
+		this.spring.register(MultiServerHttpSecurityCustomizerConfig.class).autowire();
+		Customizer<ServerHttpSecurity> httpSecurityCustomizer = this.spring.getContext()
+			.getBean("httpSecurityCustomizer", Customizer.class);
+		Customizer<ServerHttpSecurity> httpSecurityCustomizer0 = this.spring.getContext()
+			.getBean("httpSecurityCustomizer0", Customizer.class);
+		InOrder inOrder = Mockito.inOrder(httpSecurityCustomizer0, httpSecurityCustomizer);
+		ArgumentCaptor<ServerHttpSecurity> arg0 = ArgumentCaptor.forClass(ServerHttpSecurity.class);
+		inOrder.verify(httpSecurityCustomizer0).customize(arg0.capture());
+		inOrder.verify(httpSecurityCustomizer).customize(arg0.capture());
+	}
+
 	@Configuration
 	@Configuration
 	static class SubclassConfig extends ServerHttpSecurityConfiguration {
 	static class SubclassConfig extends ServerHttpSecurityConfiguration {
 
 
@@ -474,4 +521,64 @@ public class ServerHttpSecurityConfigurationTests {
 
 
 	}
 	}
 
 
+	@Configuration(proxyBeanMethods = false)
+	@EnableWebFlux
+	@EnableWebFluxSecurity
+	@Import(UserDetailsConfig.class)
+	static class AuthorizeExchangeCustomizerBeanConfig {
+
+		@Bean
+		SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
+			return http.build();
+		}
+
+		@Bean
+		static Customizer<AuthorizeExchangeSpec> authz() {
+			return mock(Customizer.class);
+		}
+
+	}
+
+	@Configuration(proxyBeanMethods = false)
+	@Import(AuthorizeExchangeCustomizerBeanConfig.class)
+	static class MultiAuthorizeExchangeCustomizerBeanConfig {
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE)
+		Customizer<AuthorizeExchangeSpec> authz0() {
+			return mock(Customizer.class);
+		}
+
+	}
+
+	@Configuration(proxyBeanMethods = false)
+	@EnableWebFlux
+	@EnableWebFluxSecurity
+	@Import(UserDetailsConfig.class)
+	static class ServerHttpSecurityCustomizerConfig {
+
+		@Bean
+		SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
+			return http.build();
+		}
+
+		@Bean
+		static Customizer<ServerHttpSecurity> httpSecurityCustomizer() {
+			return mock(Customizer.class);
+		}
+
+	}
+
+	@Configuration(proxyBeanMethods = false)
+	@Import(ServerHttpSecurityCustomizerConfig.class)
+	static class MultiServerHttpSecurityCustomizerConfig {
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE)
+		static Customizer<ServerHttpSecurity> httpSecurityCustomizer0() {
+			return mock(Customizer.class);
+		}
+
+	}
+
 }
 }

+ 33 - 0
config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt

@@ -623,5 +623,38 @@ class HttpSecurityDslTests {
 
 
     }
     }
 
 
+    @Test
+    fun `HTTP security when Dsl Bean`() {
+        this.spring.register(DslBeanConfig::class.java).autowire()
+
+        this.mockMvc.get("/")
+            .andExpect {
+                header {
+                    string("Content-Security-Policy", "object-src 'none'")
+                }
+            }
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class DslBeanConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                httpBasic { }
+            }
+            return http.build()
+        }
+
+        @Bean
+        open fun headersDsl(): HeadersDsl.() -> Unit {
+            return {
+                contentSecurityPolicy {
+                    policyDirectives = "object-src 'none'"
+                }
+            }
+        }
+    }
 
 
 }
 }

+ 69 - 0
docs/modules/ROOT/pages/reactive/configuration/webflux.adoc

@@ -242,3 +242,72 @@ It matches the requests in order by the `securityMatcher` definition.
 In this case, that means that, if the URL path starts with `/api`, Spring Security uses `apiHttpSecurity`.
 In this case, that means that, if the URL path starts with `/api`, Spring Security uses `apiHttpSecurity`.
 If the URL does not start with `/api`, Spring Security defaults to `webHttpSecurity`, which has an implied `securityMatcher` that matches any request.
 If the URL does not start with `/api`, Spring Security defaults to `webHttpSecurity`, which has an implied `securityMatcher` that matches any request.
 
 
+
+[[modular-serverhttpsecurity-configuration]]
+== Modular ServerHttpSecurity Configuration
+
+Many users prefer that their Spring Security configuration lives in a centralized place and will choose to configure it within the `SecurityWebFilterChain` Bean declaration.
+However, there are times that users may want to modularize the configuration.
+This can be done using:
+
+* xref:#serverhttpsecurity-customizer-bean[Customizer<ServerHttpSecurity> Beans]
+* xref:#top-level-customizer-bean[Top Level ServerHttpSecurity Customizer Beans]
+
+// FIXME: this needs to link to appropriate spot
+// NOTE: If you are using Spring Security's xref:servlet/configuration/kotlin.adoc[], then you can also expose `*Dsl -> Unit` Beans as outlined in xref:./kotlin.adoc#modular-httpsecuritydsl-configuration[Modular HttpSecurityDsl Configuration].
+
+
+[[serverhttpsecurity-customizer-bean]]
+=== Customizer<ServerHttpSecurity> Beans
+
+If you would like to modularize your security configuration you can place logic in a `Customizer<ServerHttpSecurity>` Bean.
+For example, the following configuration will ensure all `ServerHttpSecurity` instances are configured to:
+
+include-code::./ServerHttpSecurityCustomizerBeanConfiguration[tag=httpSecurityCustomizer,indent=0]
+
+<1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'`
+<2> xref:servlet/exploits/http.adoc#servlet-http-redirect[Redirect any request to https]
+
+
+[[top-level-customizer-bean]]
+=== Top Level ServerHttpSecurity Customizer Beans
+
+If you prefer to have further modularization of your security configuration, Spring Security will automatically apply any top level `HttpSecurity` `Customizer` Beans.
+
+A top level `HttpSecurity` `Customizer` type can be summarized as any `Customizer<T>` that matches `public HttpSecurity.*(Customizer<T>)`.
+This translates to any `Customizer<T>` that is a single argument to a public method on javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity[].
+
+A few examples can help to clarify.
+If `Customizer<ContentTypeOptionsConfig>` is published as a Bean, it will not be be automatically applied because it is an argument to javadoc:org.springframework.security.config.annotation.web.configurers.HeadersConfigurer#contentTypeOptions(org.springframework.security.config.Customizer)[] which is not a method defined on `HttpSecurity`.
+However, if `Customizer<HeadersConfigurer<HttpSecurity>>` is published as a Bean, it will be automatically applied because it is an argument to javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity#headers(org.springframework.security.config.Customizer)[].
+
+For example, the following configuration will ensure that the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] is set to `object-src 'none'`:
+
+include-code::./TopLevelCustomizerBeanConfiguration[tag=headersCustomizer,indent=0]
+
+[[customizer-bean-ordering]]
+=== Customizer Bean Ordering
+
+First each xref:#httpsecurity-customizer-bean[Customizer<HttpSecurity> Bean] is applied using https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/ObjectProvider.html#orderedStream()[ObjectProvider#orderedStream()].
+This means that if there are multiple `Customizer<HttpSecurity>` Beans, the https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/annotation/Order.html[@Order] annotation can be added to the Bean definitions to control the ordering.
+
+Next every xref:#top-level-customizer-bean[Top Level HttpSecurity Customizer Beans] type is looked up and each is is applied using `ObjectProvider#orderedStream()`.
+If there is are two `Customizer<HeadersConfigurer<HttpSecurity>>` beans and two `Customizer<HttpsRedirectConfigurer<HttpSecurity>>` instances, the order that each `Customizer` type is invoked is undefined.
+However, the order that each instance of `Customizer<HttpsRedirectConfigurer<HttpSecurity>>` is defined by `ObjectProvider#orderedStream()` and can be controlled using `@Order` on the Bean the definitions.
+
+Finally, the `HttpSecurity` Bean is injected as a Bean.
+All `Customizer` instances are applied before the `HttpSecurity` Bean is created.
+This allows overriding the customizations provided by the `Customizer` Beans.
+
+You can find an example below that illustrates the ordering:
+
+include-code::./CustomizerBeanOrderingConfiguration[tag=sample,indent=0]
+
+<1> First all `Customizer<HttpSecurity>` instances are applied.
+The `adminAuthorization` Bean has the highest `@Order` so it is applied first.
+If there are no `@Order` annotations on the `Customizer<HttpSecurity>` Beans or the `@Order` annotations had the same value, then the order that the `Customizer<HttpSecurity>` instances are applied is undefined.
+<2> The `userAuthorization` is applied next due to being an instance of `Customizer<HttpSecurity>`
+<3> The order that the `Customizer` types are undefined.
+In this example, the order of `contentSecurityPolicy`, `contentTypeOptions`, and `httpsRedirect` are undefined.
+If `@Order(Ordered.HIGHEST_PRECEDENCE)` was added to `contentTypeOptions`, then we would know that `contentTypeOptions` is before `contentSecurityPolicy` (they are the same type), but we do not know if `httpsRedirect` is before or after the `Customizer<HeadersConfigurer<HttpSecurity>>` Beans.
+<4> After all of the `Customizer` Beans are applied, the `HttpSecurity` is passed in as a Bean.

+ 69 - 0
docs/modules/ROOT/pages/servlet/configuration/java.adoc

@@ -664,6 +664,75 @@ class Config {
 ----
 ----
 ======
 ======
 
 
+[[modular-httpsecurity-configuration]]
+== Modular HttpSecurity Configuration
+
+Many users prefer that their Spring Security configuration lives in a centralized place and will choose to configure it in a single `SecurityFilterChain` instance.
+However, there are times that users may want to modularize the configuration.
+This can be done using:
+
+* xref:#httpsecurity-customizer-bean[Customizer<HttpSecurity> Beans]
+* xref:#top-level-customizer-bean[Top Level HttpSecurity Customizer Beans]
+
+NOTE: If you are using Spring Security's xref:servlet/configuration/kotlin.adoc[], then you can also expose `*Dsl -> Unit` Beans as outlined in xref:./kotlin.adoc#modular-httpsecuritydsl-configuration[Modular HttpSecurityDsl Configuration].
+
+
+[[httpsecurity-customizer-bean]]
+=== Customizer<HttpSecurity> Beans
+
+If you would like to modularize your security configuration you can place logic in a `Customizer<HttpSecurity>` Bean.
+For example, the following configuration will ensure all `HttpSecurity` instances are configured to:
+
+include-code::./HttpSecurityCustomizerBeanConfiguration[tag=httpSecurityCustomizer,indent=0]
+
+<1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'`
+<2> xref:servlet/exploits/http.adoc#servlet-http-redirect[Redirect any request to https]
+
+
+[[top-level-customizer-bean]]
+=== Top Level HttpSecurity Customizer Beans
+
+If you prefer to have further modularization of your security configuration, Spring Security will automatically apply any top level `HttpSecurity` `Customizer` Beans.
+
+A top level `HttpSecurity` `Customizer` type can be summarized as any `Customizer<T>` that matches `public HttpSecurity.*(Customizer<T>)`.
+This translates to any `Customizer<T>` that is a single argument to a public method on javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity[].
+
+A few examples can help to clarify.
+If `Customizer<ContentTypeOptionsConfig>` is published as a Bean, it will not be be automatically applied because it is an argument to javadoc:org.springframework.security.config.annotation.web.configurers.HeadersConfigurer#contentTypeOptions(org.springframework.security.config.Customizer)[] which is not a method defined on `HttpSecurity`.
+However, if `Customizer<HeadersConfigurer<HttpSecurity>>` is published as a Bean, it will be automatically applied because it is an argument to javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity#headers(org.springframework.security.config.Customizer)[].
+
+For example, the following configuration will ensure that the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] is set to `object-src 'none'`:
+
+include-code::./TopLevelCustomizerBeanConfiguration[tag=headersCustomizer,indent=0]
+
+[[customizer-bean-ordering]]
+=== Customizer Bean Ordering
+
+First each xref:#httpsecurity-customizer-bean[Customizer<HttpSecurity> Bean] is applied using https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/ObjectProvider.html#orderedStream()[ObjectProvider#orderedStream()].
+This means that if there are multiple `Customizer<HttpSecurity>` Beans, the https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/annotation/Order.html[@Order] annotation can be added to the Bean definitions to control the ordering.
+
+Next every xref:#top-level-customizer-bean[Top Level HttpSecurity Customizer Beans] type is looked up and each is is applied using `ObjectProvider#orderedStream()`.
+If there is are two `Customizer<HeadersConfigurer<HttpSecurity>>` beans and two `Customizer<HttpsRedirectConfigurer<HttpSecurity>>` instances, the order that each `Customizer` type is invoked is undefined.
+However, the order that each instance of `Customizer<HttpsRedirectConfigurer<HttpSecurity>>` is defined by `ObjectProvider#orderedStream()` and can be controlled using `@Order` on the Bean the definitions.
+
+Finally, the `HttpSecurity` Bean is injected as a Bean.
+All `Customizer` instances are applied before the `HttpSecurity` Bean is created.
+This allows overriding the customizations provided by the `Customizer` Beans.
+
+You can find an example below that illustrates the ordering:
+
+include-code::./CustomizerBeanOrderingConfiguration[tag=sample,indent=0]
+
+<1> First all `Customizer<HttpSecurity>` instances are applied.
+The `adminAuthorization` Bean has the highest `@Order` so it is applied first.
+If there are no `@Order` annotations on the `Customizer<HttpSecurity>` Beans or the `@Order` annotations had the same value, then the order that the `Customizer<HttpSecurity>` instances are applied is undefined.
+<2> The `userAuthorization` is applied next due to being an instance of `Customizer<HttpSecurity>`
+<3> The order that the `Customizer` types are undefined.
+In this example, the order of `contentSecurityPolicy`, `contentTypeOptions`, and `httpsRedirect` are undefined.
+If `@Order(Ordered.HIGHEST_PRECEDENCE)` was added to `contentTypeOptions`, then we would know that `contentTypeOptions` is before `contentSecurityPolicy` (they are the same type), but we do not know if `httpsRedirect` is before or after the `Customizer<HeadersConfigurer<HttpSecurity>>` Beans.
+<4> After all of the `Customizer` Beans are applied, the `HttpSecurity` is passed in as a Bean.
+
+
 [[post-processing-configured-objects]]
 [[post-processing-configured-objects]]
 == Post Processing Configured Objects
 == Post Processing Configured Objects
 
 

+ 73 - 0
docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc

@@ -346,3 +346,76 @@ class BankingSecurityConfig {
 	This configuration will handle requests not covered by the other filter chains and will be processed last (no `@Order` defaults to last).
 	This configuration will handle requests not covered by the other filter chains and will be processed last (no `@Order` defaults to last).
 	Requests that match `/`, `/user-login`, `/user-logout`, `/notices`, `/contact` and `/register` allow access without authentication.
 	Requests that match `/`, `/user-login`, `/user-logout`, `/notices`, `/contact` and `/register` allow access without authentication.
 	Any other requests require the user to be authenticated to access any URL not explicitly allowed or protected by other filter chains.
 	Any other requests require the user to be authenticated to access any URL not explicitly allowed or protected by other filter chains.
+
+
+[[modular-httpsecuritydsl-configuration]]
+== Modular HttpSecurityDsl Configuration
+
+Many users prefer that their Spring Security configuration lives in a centralized place and will choose to configure it in a single `SecurityFilterChain` instance.
+However, there are times that users may want to modularize the configuration.
+This can be done using:
+
+* xref:#httpsecuritydsl-bean[HttpSecurityDsl.() -> Unit Beans]
+* xref:#top-level-dsl-bean[Top Level Security Dsl Beans]
+
+NOTE: Since the Spring Security Kotlin Dsl (`HttpSecurityDsl`) uses `HttpSecurity`, all of the Java xref:./kotlin.adoc#modular-bean-configuration[Modular Bean Customization] is applied before xref:#modular-httpsecuritydsl-configuration[Modular HttpSecurity Configuration].
+
+[[httpsecuritydsl-bean]]
+=== HttpSecurityDsl.() -> Unit Beans
+
+If you would like to modularize your security configuration you can place logic in a `HttpSecurityDsl.() -> Unit` Bean.
+For example, the following configuration will ensure all `HttpSecurityDsl` instances are configured to:
+
+include-code::./HttpSecurityDslBeanConfiguration[tag=httpSecurityDslBean,indent=0]
+
+<1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'`
+<2> xref:servlet/exploits/http.adoc#servlet-http-redirect[Redirect any request to https]
+
+
+[[top-level-dsl-bean]]
+=== Top Level Security Dsl Beans
+
+If you prefer to have further modularization of your security configuration, Spring Security will automatically apply any top level Security Dsl Beans.
+
+A top level Security Dsl can be summarized as any class Dsl class that matches `public HttpSecurityDsl.*(<Dsl>)`.
+This translates to any Security Dsl that is a single argument to a public method on `HttpSecurityDsl`.
+
+A few examples can help to clarify.
+If `ContentTypeOptionsDsl.() -> Unit` is published as a Bean, it will not be be automatically applied because it is an argument to `HeadersDsl#contentTypeOptions(ContentTypeOptionsDsl.() -> Unit)` and is not an argument to a method defined on `HttpSecurityDsl`.
+However, if `HeadersDsl.() -> Unit` is published as a Bean, it will be automatically applied because it is an argument to `HttpSecurityDsl.headers(HeadersDsl.() -> Unit)`.
+
+For example, the following configuration ensure all `HttpSecurityDsl` instances are configured to:
+
+include-code::./TopLevelDslBeanConfiguration[tag=headersSecurity,indent=0]
+
+<1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'`
+
+[[dsl-bean-ordering]]
+=== Dsl Bean Ordering
+
+First, all xref:servlet/configuration/java.adoc#modular-httpsecurity-configuration[Modular HttpSecurity Configuration] is applied since the Kotlin Dsl uses an `HttpSecurity` Bean.
+
+Second, each xref:#httpsecuritydsl-bean[HttpSecurityDsl.() -> Unit Beans] is applied using https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/ObjectProvider.html#orderedStream()[ObjectProvider#orderedStream()].
+This means that if there are multiple `HttpSecurity.() -> Unit` Beans, the https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/annotation/Order.html[@Order] annotation can be added to the Bean definitions to control the ordering.
+
+Next, every xref:#top-level-dsl-bean[Top Level Security Dsl Beans] type is looked up and each is is applied using `ObjectProvider#orderedStream()`.
+If there is are differt types of top level security Beans (.e.g. `HeadersDsl.() -> Unit` and `HttpsRedirectDsl.() -> Unit`), then the order that each Dsl type is invoked is undefined.
+However, the order that each instance of of the same top level security Bean type is defined by `ObjectProvider#orderedStream()` and can be controlled using `@Order` on the Bean the definitions.
+
+Finally, the `HttpSecurityDsl` Bean is injected as a Bean.
+All `*Dsl.() -> Unit` Beans are applied before the `HttpSecurityDsl` Bean is created.
+This allows overriding the customizations provided by the `*Dsl.() -> Unit` Beans.
+
+You can find an example below that illustrates the ordering:
+
+include-code::./DslBeanOrderingConfiguration[tag=sample,indent=0]
+
+<1> All xref:servlet/configuration/java.adoc#modular-httpsecurity-configuration[Modular HttpSecurity Configuration] is applied since the Kotlin Dsl uses an `HttpSecurity` Bean.
+<2> All `HttpSecurity.() -> Unit` instances are applied.
+The `adminAuthorization` Bean has the highest `@Order` so it is applied first.
+If there are no `@Order` annotations on the `HttpSecurity.() -> Unit` Beans or the `@Order` annotations had the same value, then the order that the `HttpSecurity.() -> Unit` instances are applied is undefined.
+<3> The `userAuthorization` is applied next due to being an instance of `HttpSecurity.() -> Unit`
+<4> The order that the `*Dsl.() -> Unit` types are undefined.
+In this example, the order of `contentSecurityPolicy`, `contentTypeOptions`, and `httpsRedirect` are undefined.
+If `@Order(Ordered.HIGHEST_PRECEDENCE)` was added to `contentTypeOptions`, then we would know that `contentTypeOptions` is before `contentSecurityPolicy` (they are the same type), but we do not know if `httpsRedirect` is before or after the `HeadersDsl.() -> Unit` Beans.
+<5> After all of the `*Dsl.() -> Unit` Beans are applied, the `HttpSecurityDsl` is passed in as a Bean.

+ 1 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -15,6 +15,7 @@ Each section that follows will indicate the more notable removals as well as the
 
 
 == Config
 == Config
 
 
+* Support modular modular configuration in xref::servlet/configuration/java.adoc#modular-httpsecurity-configuration[Servlets] and xref::reactive/configuration/webflux.adoc#modular-serverhttpsecurity-configuration[WebFlux]
 * Removed `and()` from the `HttpSecurity` DSL in favor of using the lambda methods
 * Removed `and()` from the `HttpSecurity` DSL in favor of using the lambda methods
 * Removed `authorizeRequests` in favor of `authorizeHttpRequests`
 * Removed `authorizeRequests` in favor of `authorizeHttpRequests`
 * Simplified expression migration for `authorizeRequests`
 * Simplified expression migration for `authorizeRequests`

+ 99 - 0
docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java

@@ -0,0 +1,99 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.reactive.configuration.customizerbeanordering;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.web.reactive.config.EnableWebFlux;
+
+/**
+ *
+ */
+@EnableWebFlux
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+class CustomizerBeanOrderingConfiguration {
+
+	// tag::sample[]
+	@Bean // <4>
+	SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
+		// @formatter:off
+		http
+			.authorizeExchange((exchange) -> exchange
+				.anyExchange().authenticated()
+			);
+		return http.build();
+		// @formatter:on
+	}
+
+	@Bean
+	@Order(Ordered.LOWEST_PRECEDENCE) // <2>
+	Customizer<ServerHttpSecurity> userAuthorization() {
+		// @formatter:off
+		return (http) -> http
+			.authorizeExchange((exchange) -> exchange
+				.pathMatchers("/users/**").hasRole("USER")
+			);
+		// @formatter:on
+	}
+
+	@Bean
+	@Order(Ordered.HIGHEST_PRECEDENCE) // <1>
+	Customizer<ServerHttpSecurity> adminAuthorization() {
+		// @formatter:off
+		return (http) -> http
+			.authorizeExchange((exchange) -> exchange
+				.pathMatchers("/admins/**").hasRole("ADMIN")
+			);
+		// @formatter:on
+	}
+
+	// <3>
+
+	@Bean
+	Customizer<ServerHttpSecurity.HeaderSpec> contentSecurityPolicy() {
+		// @formatter:off
+		return (headers) -> headers
+			.contentSecurityPolicy((csp) -> csp
+				.policyDirectives("object-src 'none'")
+			);
+		// @formatter:on
+	}
+
+	@Bean
+	Customizer<ServerHttpSecurity.HeaderSpec> contentTypeOptions() {
+		// @formatter:off
+		return (headers) -> headers
+			.contentTypeOptions(Customizer.withDefaults());
+		// @formatter:on
+	}
+
+	@Bean
+	Customizer<ServerHttpSecurity.HttpsRedirectSpec> httpsRedirect() {
+		// @formatter:off
+		return Customizer.withDefaults();
+		// @formatter:on
+	}
+	// end::sample[]
+
+}

+ 76 - 0
docs/src/test/java/org/springframework/security/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java

@@ -0,0 +1,76 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.reactive.configuration.customizerbeanordering;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class CustomizerBeanOrderingTests {
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	private WebTestClient webTest;
+
+	@Test
+	void authorizationOrdered() throws Exception {
+		this.spring.register(
+				CustomizerBeanOrderingConfiguration.class).autowire();
+		// @formatter:off
+		this.webTest.mutateWith(mockUser("admin").roles("ADMIN"))
+			.get()
+			.uri("https://localhost/admins/1")
+			.exchange()
+			.expectStatus().isOk();
+		this.webTest.mutateWith(mockUser("user").roles("USER"))
+			.get()
+			.uri("https://localhost/admins/1")
+			.exchange()
+			.expectStatus().isForbidden();
+		this.webTest.mutateWith(mockUser("user").roles("USER"))
+			.get()
+			.uri("https://localhost/users/1")
+			.exchange()
+			.expectStatus().isOk();
+		this.webTest.mutateWith(mockUser("user").roles("OTHER"))
+			.get()
+			.uri("https://localhost/users/1")
+			.exchange()
+			.expectStatus().isForbidden();
+		this.webTest.mutateWith(mockUser("authenticated").roles("OTHER"))
+				.get()
+				.uri("https://localhost/other")
+				.exchange()
+				.expectStatus().isOk();
+		// @formatter:on
+	}
+
+}

+ 61 - 0
docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.reactive.configuration.serverhttpsecuritycustomizerbean;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+
+/**
+ *
+ */
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+class ServerHttpSecurityCustomizerBeanConfiguration {
+
+	@Bean
+	SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
+		// @formatter:off
+		http
+			.authorizeExchange((exchange) -> exchange
+				.anyExchange().authenticated()
+			);
+		return http.build();
+		// @formatter:on
+	}
+
+	// tag::httpSecurityCustomizer[]
+	@Bean
+	Customizer<ServerHttpSecurity> httpSecurityCustomizer() {
+		// @formatter:off
+		return (http) -> http
+			.headers((headers) -> headers
+				.contentSecurityPolicy((csp) -> csp
+					// <1>
+					.policyDirectives("object-src 'none'")
+				)
+			)
+			// <2>
+			.redirectToHttps(Customizer.withDefaults());
+		// @formatter:on
+	}
+	// end::httpSecurityCustomizer[]
+
+}

+ 56 - 0
docs/src/test/java/org/springframework/security/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.reactive.configuration.serverhttpsecuritycustomizerbean;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class ServerHttpSecurityCustomizerBeanTests {
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	private WebTestClient webTest;
+
+	@Test
+	void httpSecurityCustomizer() throws Exception {
+		this.spring.register(
+				ServerHttpSecurityCustomizerBeanConfiguration.class).autowire();
+		// @formatter:off
+		this.webTest
+			.get()
+			.uri("http://localhost/")
+			.exchange()
+			.expectHeader().location("https://localhost/")
+			.expectHeader()
+				.value("Content-Security-Policy", csp ->
+					assertThat(csp).isEqualTo("object-src 'none'")
+				);
+		// @formatter:on
+	}
+
+}

+ 58 - 0
docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.reactive.configuration.toplevelcustomizerbean;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.web.reactive.config.EnableWebFlux;
+
+/**
+ *
+ */
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+public class TopLevelCustomizerBeanConfiguration {
+
+	@Bean
+	SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
+		// @formatter:off
+		http
+			.authorizeExchange((exchange) -> exchange
+				.anyExchange().authenticated()
+			);
+		return http.build();
+		// @formatter:on
+	}
+
+	// tag::headersCustomizer[]
+	@Bean
+	Customizer<ServerHttpSecurity.HeaderSpec> headersSecurity() {
+		// @formatter:off
+		return (headers) -> headers
+			.contentSecurityPolicy((csp) -> csp
+				// <1>
+				.policyDirectives("object-src 'none'")
+			);
+		// @formatter:on
+	}
+	// end::headersCustomizer[]
+
+}

+ 61 - 0
docs/src/test/java/org/springframework/security/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.reactive.configuration.toplevelcustomizerbean;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.test.web.servlet.ResultMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class TopLevelCustomizerBeanTests {
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	private WebTestClient webTest;
+
+
+	@Test
+	void headersCustomizer() throws Exception {
+		this.spring.register(TopLevelCustomizerBeanConfiguration.class).autowire();
+		// @formatter:off
+		this.webTest
+			.get()
+			.uri("http://localhost/")
+			.exchange()
+			.expectHeader()
+				.value("Content-Security-Policy", csp ->
+						assertThat(csp).isEqualTo("object-src 'none'")
+				);
+		// @formatter:on
+	}
+
+	private static @NotNull ResultMatcher cspIsObjectSrcNone() {
+		return header().string("Content-Security-Policy", "object-src 'none'");
+	}
+}

+ 102 - 0
docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.servlet.configuration.customizerbeanordering;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.ThrowingCustomizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+/**
+ *
+ */
+@EnableWebMvc
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+class CustomizerBeanOrderingConfiguration {
+
+	// tag::sample[]
+	@Bean // <4>
+	SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
+		// @formatter:off
+		http
+			.authorizeHttpRequests((requests) -> requests
+				.anyRequest().authenticated()
+			);
+		return http.build();
+		// @formatter:on
+	}
+
+	@Bean
+	@Order(Ordered.LOWEST_PRECEDENCE) // <2>
+	ThrowingCustomizer<HttpSecurity> userAuthorization() {
+		// @formatter:off
+		return (http) -> http
+			.authorizeHttpRequests((requests) -> requests
+				.requestMatchers("/users/**").hasRole("USER")
+			);
+		// @formatter:on
+	}
+
+	@Bean
+	@Order(Ordered.HIGHEST_PRECEDENCE) // <1>
+	ThrowingCustomizer<HttpSecurity> adminAuthorization() {
+		// @formatter:off
+		return (http) -> http
+			.authorizeHttpRequests((requests) -> requests
+				.requestMatchers("/admins/**").hasRole("ADMIN")
+			);
+		// @formatter:on
+	}
+
+	// <3>
+
+	@Bean
+	Customizer<HeadersConfigurer<HttpSecurity>> contentSecurityPolicy() {
+		// @formatter:off
+		return (headers) -> headers
+			.contentSecurityPolicy((csp) -> csp
+				.policyDirectives("object-src 'none'")
+			);
+		// @formatter:on
+	}
+
+	@Bean
+	Customizer<HeadersConfigurer<HttpSecurity>> contentTypeOptions() {
+		// @formatter:off
+		return (headers) -> headers
+			.contentTypeOptions(Customizer.withDefaults());
+		// @formatter:on
+	}
+
+	@Bean
+	Customizer<HttpsRedirectConfigurer<HttpSecurity>> httpsRedirect() {
+		// @formatter:off
+		return Customizer.withDefaults();
+		// @formatter:on
+	}
+	// end::sample[]
+
+}

+ 64 - 0
docs/src/test/java/org/springframework/security/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.servlet.configuration.customizerbeanordering;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class CustomizerBeanOrderingTests {
+	public final SpringTestContext spring = new SpringTestContext(this).mockMvcAfterSpringSecurityOk();
+
+	@Autowired
+	private MockMvc mockMvc;
+
+	@Test
+	void authorizationOrdered() throws Exception {
+		this.spring.register(
+				CustomizerBeanOrderingConfiguration.class).autowire();
+		// @formatter:off
+		this.mockMvc
+			.perform(get("https://localhost/admins/1").with(user("admin").roles("ADMIN")))
+			.andExpect(status().isOk());
+		this.mockMvc
+				.perform(get("https://localhost/admins/1").with(user("user").roles("USER")))
+				.andExpect(status().isForbidden());
+		this.mockMvc
+				.perform(get("https://localhost/users/1").with(user("user").roles("USER")))
+				.andExpect(status().isOk());
+		this.mockMvc
+				.perform(get("https://localhost/users/1").with(user("user").roles("OTHER")))
+				.andExpect(status().isForbidden());
+		this.mockMvc
+				.perform(get("https://localhost/other").with(user("authenticated").roles("OTHER")))
+				.andExpect(status().isOk());
+		// @formatter:on
+	}
+
+}

+ 77 - 0
docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.servlet.configuration.httpsecuritycustomizerbean;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.ThrowingCustomizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+
+/**
+ *
+ */
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+class HttpSecurityCustomizerBeanConfiguration {
+
+	@Bean
+	SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
+		// @formatter:off
+		http
+			.authorizeHttpRequests((requests) -> requests
+				.anyRequest().authenticated()
+			);
+		return http.build();
+		// @formatter:on
+	}
+
+	// tag::httpSecurityCustomizer[]
+	@Bean
+	ThrowingCustomizer<HttpSecurity> httpSecurityCustomizer() {
+		// @formatter:off
+		return (http) -> http
+			.headers((headers) -> headers
+				.contentSecurityPolicy((csp) -> csp
+					// <1>
+					.policyDirectives("object-src 'none'")
+				)
+			)
+			// <2>
+			.redirectToHttps(Customizer.withDefaults());
+		// @formatter:on
+	}
+	// end::httpSecurityCustomizer[]
+
+}

+ 74 - 0
docs/src/test/java/org/springframework/security/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.servlet.configuration.httpsecuritycustomizerbean;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.ThrowingCustomizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class HttpSecurityCustomizerBeanTests {
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	private MockMvc mockMvc;
+
+	@Test
+	void httpSecurityCustomizer() throws Exception {
+		this.spring.register(HttpSecurityCustomizerBeanConfiguration.class).autowire();
+		// @formatter:off
+		this.mockMvc
+			.perform(get("/"))
+			.andExpect(redirectsToHttps());
+		// headers are not sent back as a part of the redirect to https, so a separate request is necessary
+		this.mockMvc.perform(get("https://localhost/"))
+			.andExpect(cspIsObjectSrcNone());
+		// @formatter:on
+	}
+
+	private static @NotNull ResultMatcher redirectsToHttps() {
+		return mvcResult -> assertThat(
+			mvcResult.getResponse().getRedirectedUrl()).startsWith("https://");
+	}
+
+	private static @NotNull ResultMatcher cspIsObjectSrcNone() {
+		return header().string("Content-Security-Policy", "object-src 'none'");
+	}
+
+}

+ 71 - 0
docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.servlet.configuration.toplevelcustomizerbean;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.ThrowingCustomizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+
+/**
+ *
+ */
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+public class TopLevelCustomizerBeanConfiguration {
+
+	@Bean
+	SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
+		// @formatter:off
+		http
+			.authorizeHttpRequests((requests) -> requests
+				.anyRequest().authenticated()
+			);
+		return http.build();
+		// @formatter:on
+	}
+
+	// tag::headersCustomizer[]
+	@Bean
+	Customizer<HeadersConfigurer<HttpSecurity>> headersSecurity() {
+		// @formatter:off
+		return (headers) -> headers
+			.contentSecurityPolicy((csp) -> csp
+				// <1>
+				.policyDirectives("object-src 'none'")
+			);
+		// @formatter:on
+	}
+	// end::headersCustomizer[]
+
+}

+ 66 - 0
docs/src/test/java/org/springframework/security/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.servlet.configuration.toplevelcustomizerbean;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.ThrowingCustomizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class TopLevelCustomizerBeanTests {
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	private MockMvc mockMvc;
+
+
+	@Test
+	void headersCustomizer() throws Exception {
+		this.spring.register(TopLevelCustomizerBeanConfiguration.class).autowire();
+		// @formatter:off
+		this.mockMvc
+			.perform(get("/"))
+			.andExpect(cspIsObjectSrcNone());
+		// @formatter:on
+	}
+
+	private static @NotNull ResultMatcher cspIsObjectSrcNone() {
+		return header().string("Content-Security-Policy", "object-src 'none'");
+	}
+}

+ 104 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt

@@ -0,0 +1,104 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kt.docs.reactive.configuration.customizerbeanordering
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.Ordered
+import org.springframework.core.annotation.Order
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.ThrowingCustomizer
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer
+import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.anyExchange
+
+/**
+ *
+ */
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+internal class CustomizerBeanOrderingConfiguration {
+    // tag::sample[]
+    @Bean // <4>
+    fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
+        // @formatter:off
+        http
+            .authorizeExchange({ exchanges -> exchanges
+                .anyExchange().authenticated()
+            })
+        return http.build()
+        // @formatter:on
+    }
+
+    @Bean
+    @Order(Ordered.LOWEST_PRECEDENCE)  // <2>
+    fun userAuthorization(): Customizer<ServerHttpSecurity> {
+        // @formatter:off
+        return Customizer { http -> http
+            .authorizeExchange { exchanges -> exchanges
+                .pathMatchers("/users/**").hasRole("USER")
+            }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    @Order(Ordered.HIGHEST_PRECEDENCE) // <1>
+    fun adminAuthorization(): Customizer<ServerHttpSecurity> {
+        // @formatter:off
+        return ThrowingCustomizer { http -> http
+            .authorizeExchange { exchanges -> exchanges
+                .pathMatchers("/admins/**").hasRole("ADMIN")
+            }
+        }
+        // @formatter:on
+    }
+
+    // <3>
+
+    @Bean
+    fun contentSecurityPolicy(): Customizer<ServerHttpSecurity.HeaderSpec> {
+        // @formatter:off
+        return Customizer { headers -> headers
+            .contentSecurityPolicy { csp -> csp
+                .policyDirectives("object-src 'none'")
+            }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    fun contentTypeOptions(): Customizer<ServerHttpSecurity.HeaderSpec> {
+        // @formatter:off
+        return Customizer { headers -> headers
+            .contentTypeOptions(Customizer.withDefaults())
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    fun httpsRedirect(): Customizer<ServerHttpSecurity.HttpsRedirectSpec> {
+        // @formatter:off
+        return Customizer.withDefaults()
+        // @formatter:on
+    }
+    // end::sample[]
+}

+ 74 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kt.docs.reactive.configuration.customizerbeanordering
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class CustomizerBeanOrderingTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var webTest: WebTestClient
+
+    @Test
+    fun authorizationOrdered() {
+        this.spring.register(CustomizerBeanOrderingConfiguration::class.java).autowire()
+        // @formatter:off
+        this.webTest.mutateWith(mockUser("admin").roles("ADMIN"))
+            .get()
+            .uri("https://localhost/admins/1")
+            .exchange()
+            .expectStatus().isOk
+        this.webTest.mutateWith(mockUser("user").roles("USER"))
+            .get()
+            .uri("https://localhost/admins/1")
+            .exchange()
+            .expectStatus().isForbidden
+        this.webTest.mutateWith(mockUser("user").roles("USER"))
+            .get()
+            .uri("https://localhost/users/1")
+            .exchange()
+            .expectStatus().isOk
+        this.webTest.mutateWith(mockUser("user").roles("OTHER"))
+            .get()
+            .uri("https://localhost/users/1")
+            .exchange()
+             .expectStatus().isForbidden
+        this.webTest.mutateWith(mockUser("other").roles("OTHER"))
+            .get()
+            .uri("https://localhost/other")
+            .exchange()
+            .expectStatus().isOk
+        // @formatter:on
+    }
+}

+ 99 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt

@@ -0,0 +1,99 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kt.docs.reactive.configuration.dslbeanordering
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.Ordered
+import org.springframework.core.annotation.Order
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.*
+import org.springframework.security.web.server.SecurityWebFilterChain
+
+/**
+ *
+ */
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+internal class DslBeanOrderingConfiguration {
+    // tag::sample[]
+    // All of the Java Modular Configuration is applied first <1>
+
+    @Bean // <5>
+    fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
+        // @formatter:off
+        return http {
+            authorizeExchange {
+                authorize(anyExchange, authenticated)
+            }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    @Order(Ordered.LOWEST_PRECEDENCE)  // <3>
+    fun userAuthorization(): ServerHttpSecurityDsl.() -> Unit {
+        // @formatter:off
+        return {
+            authorizeExchange {
+                authorize("/users/**", hasRole("USER"))
+            }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    @Order(Ordered.HIGHEST_PRECEDENCE) // <2>
+    fun adminAuthorization(): ServerHttpSecurityDsl.() -> Unit {
+        // @formatter:off
+        return {
+            authorizeExchange {
+                authorize("/admins/**", hasRole("ADMIN"))
+            }
+        }
+        // @formatter:on
+    }
+
+    // <4>
+
+    @Bean
+    fun contentSecurityPolicy(): ServerHeadersDsl.() -> Unit {
+        // @formatter:off
+        return {
+            contentSecurityPolicy {
+                policyDirectives = "object-src 'none'"
+            }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    fun contentTypeOptions(): ServerHeadersDsl.() -> Unit {
+        // @formatter:off
+        return {
+            contentTypeOptions { }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    fun httpsRedirect(): ServerHttpsRedirectDsl.() -> Unit {
+        // @formatter:off
+        return { }
+        // @formatter:on
+    }
+    // end::sample[]
+}

+ 72 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/dslbeanordering/DslBeanOrderingTests.kt

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kt.docs.reactive.configuration.dslbeanordering
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.kt.docs.servlet.configuration.customizerbeanordering.CustomizerBeanOrderingConfiguration
+import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class DslBeanOrderingTests {
+    @JvmField
+    val spring = SpringTestContext(this).mockMvcAfterSpringSecurityOk()
+
+    @Autowired
+    lateinit var webTest: WebTestClient
+
+    @Test
+    fun dslOrdered() {
+        this.spring.register(org.springframework.security.kt.docs.reactive.configuration.customizerbeanordering.CustomizerBeanOrderingConfiguration::class.java).autowire()
+        // @formatter:off
+        this.webTest.mutateWith(mockUser("admin").roles("ADMIN"))
+            .get()
+            .uri("https://localhost/admins/1")
+            .exchange()
+            .expectStatus().isOk
+        this.webTest.mutateWith(mockUser("user").roles("USER"))
+            .get()
+            .uri("https://localhost/admins/1")
+            .exchange()
+            .expectStatus().isForbidden
+        this.webTest.mutateWith(mockUser("user").roles("USER"))
+            .get()
+            .uri("https://localhost/users/1")
+            .exchange()
+            .expectStatus().isOk
+        this.webTest.mutateWith(mockUser("user").roles("OTHER"))
+            .get()
+            .uri("https://localhost/users/1")
+            .exchange()
+            .expectStatus().isForbidden
+        this.webTest.mutateWith(mockUser("other").roles("OTHER"))
+            .get()
+            .uri("https://localhost/other")
+            .exchange()
+            .expectStatus().isOk
+        // @formatter:on
+    }
+}

+ 44 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanConfiguration.kt

@@ -0,0 +1,44 @@
+package org.springframework.security.kt.docs.reactive.configuration.serverhttpsecuritycustomizerbean
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.SecurityWebFilterChain
+
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+class ServerHttpSecurityCustomizerBeanConfiguration {
+
+    @Bean
+    fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
+        // @formatter:off
+        http
+            .authorizeExchange({ exchanges -> exchanges
+                .anyExchange().authenticated()
+            })
+        return http.build()
+        // @formatter:on
+    }
+
+
+    // tag::httpSecurityCustomizer[]
+    @Bean
+    fun httpSecurityCustomizer(): Customizer<ServerHttpSecurity> {
+        // @formatter:off
+        return Customizer { http -> http
+            .headers { headers -> headers
+                .contentSecurityPolicy { csp -> csp
+                    // <1>
+                    .policyDirectives("object-src 'none'")
+                }
+            }
+            // <2>
+            .redirectToHttps(Customizer.withDefaults())
+        }
+        // @formatter:on
+    }
+    // end::httpSecurityCustomizer[]
+
+}

+ 37 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritycustomizerbean/ServerHttpSecurityCustomizerBeanTests.kt

@@ -0,0 +1,37 @@
+package org.springframework.security.kt.docs.reactive.configuration.serverhttpsecuritycustomizerbean
+
+import org.assertj.core.api.Assertions
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.test.web.reactive.server.WebTestClient
+import java.util.function.Consumer
+
+@ExtendWith(SpringTestContextExtension::class)
+class ServerHttpSecurityCustomizerBeanTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var webTest: WebTestClient
+
+    @Test
+    fun `serverhttpsecurity customizer config`() {
+        this.spring.register(ServerHttpSecurityCustomizerBeanConfiguration::class.java).autowire()
+        // @formatter:off
+        this.webTest
+            .get()
+            .uri("http://localhost/")
+            .exchange()
+            .expectHeader().location("https://localhost/")
+            .expectHeader()
+            .value("Content-Security-Policy", Consumer { csp ->
+                assertThat(csp).isEqualTo("object-src 'none'")
+            })
+            // @formatter:on
+    }
+
+}

+ 42 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanConfiguration.kt

@@ -0,0 +1,42 @@
+package org.springframework.security.kt.docs.reactive.configuration.serverhttpsecuritydslbean
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurityDsl
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+class ServerHttpSecurityDslBeanConfiguration {
+
+    @Bean
+    fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
+        return http {
+            authorizeExchange {
+                authorize(anyExchange, authenticated)
+            }
+        }
+    }
+
+    // tag::httpSecurityDslBean[]
+    @Bean
+    fun httpSecurityDslBean(): ServerHttpSecurityDsl.() -> Unit {
+        return {
+            headers {
+                contentSecurityPolicy {
+                    // <1>
+                    policyDirectives = "object-src 'none'"
+                }
+            }
+            // <2>
+            redirectToHttps { }
+        }
+    }
+    // end::httpSecurityDslBean[]
+
+}

+ 38 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/serverhttpsecuritydslbean/ServerHttpSecurityDslBeanTests.kt

@@ -0,0 +1,38 @@
+package org.springframework.security.kt.docs.reactive.configuration.serverhttpsecuritydslbean
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import java.util.function.Consumer
+
+
+@ExtendWith(SpringTestContextExtension::class)
+class ServerHttpSecurityDslBeanTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var webTest: WebTestClient
+
+    @Test
+    fun `ServerHttpSecurityDslBean`() {
+        this.spring.register(ServerHttpSecurityDslBeanConfiguration::class.java).autowire()
+
+        // @formatter:off
+        this.webTest
+            .get()
+            .uri("http://localhost/")
+            .exchange()
+            .expectHeader().location("https://localhost/")
+            .expectHeader().value("Content-Security-Policy", Consumer { csp ->
+                assertThat(csp).isEqualTo("object-src 'none'")
+            })
+        // @formatter:on
+    }
+}

+ 43 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt

@@ -0,0 +1,43 @@
+package org.springframework.security.kt.docs.reactive.configuration.toplevelcustomizerbean
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.server.SecurityWebFilterChain
+
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+class TopLevelCustomizerBeanConfiguration {
+
+    @Bean
+    fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
+        // @formatter:off
+        http
+            .authorizeExchange({ exchanges -> exchanges
+                .anyExchange().authenticated()
+            })
+        return http.build()
+        // @formatter:on
+    }
+
+    // tag::headersCustomizer[]
+    @Bean
+    fun headersSecurity(): Customizer<ServerHttpSecurity.HeaderSpec> {
+        // @formatter:off
+        return Customizer { headers -> headers
+            .contentSecurityPolicy { csp -> csp
+                // <1>
+                .policyDirectives("object-src 'none'")
+            }
+        }
+        // @formatter:on
+    }
+    // end::headersCustomizer[]
+
+}

+ 35 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt

@@ -0,0 +1,35 @@
+package org.springframework.security.kt.docs.reactive.configuration.toplevelcustomizerbean
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.test.web.reactive.server.WebTestClient
+import java.util.function.Consumer
+
+@ExtendWith(SpringTestContextExtension::class)
+class TopLevelCustomizerBeanTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var webTest: WebTestClient
+
+    @Test
+    fun `top level dsl bean`() {
+        this.spring.register(TopLevelCustomizerBeanConfiguration::class.java).autowire()
+
+        // @formatter:off
+        this.webTest
+            .get()
+            .uri("http://localhost/")
+            .exchange()
+            .expectHeader().value("Content-Security-Policy", Consumer { csp ->
+                assertThat(csp).isEqualTo("object-src 'none'")
+            })
+        // @formatter:on
+    }
+
+}

+ 36 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt

@@ -0,0 +1,36 @@
+package org.springframework.security.kt.docs.reactive.configuration.topleveldslbean
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHeadersDsl
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.web.server.SecurityWebFilterChain
+
+
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+class TopLevelDslBeanConfiguration {
+
+    @Bean
+    fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
+        return http {
+            authorizeExchange {
+                authorize(anyExchange, authenticated)
+            }
+        }
+    }
+
+    // tag::headersSecurity[]
+    @Bean
+    fun headersSecurity(): ServerHeadersDsl.() -> Unit {
+        return {
+            contentSecurityPolicy {
+                // <1>
+                policyDirectives = "object-src 'none'"
+            }
+        }
+    }
+    // end::headersSecurity[]
+}

+ 37 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/configuration/topleveldslbean/TopLevelDslBeanTests.kt

@@ -0,0 +1,37 @@
+package org.springframework.security.kt.docs.reactive.configuration.topleveldslbean
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import java.util.function.Consumer
+
+
+@ExtendWith(SpringTestContextExtension::class)
+class TopLevelDslBeanTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var webTest: WebTestClient
+
+    @Test
+    fun `HttpSecurityDslBean`() {
+        this.spring.register(TopLevelDslBeanConfiguration::class.java).autowire()
+
+        // @formatter:off
+        this.webTest
+            .get()
+            .uri("http://localhost/")
+            .exchange()
+            .expectHeader().value("Content-Security-Policy", Consumer { csp ->
+                assertThat(csp).isEqualTo("object-src 'none'")
+            })
+        // @formatter:on
+    }
+}

+ 104 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingConfiguration.kt

@@ -0,0 +1,104 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kt.docs.servlet.configuration.customizerbeanordering
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.Ordered
+import org.springframework.core.annotation.Order
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.ThrowingCustomizer
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer
+import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+/**
+ *
+ */
+@EnableWebMvc
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+internal class CustomizerBeanOrderingConfiguration {
+    // tag::sample[]
+    @Bean // <4>
+    fun springSecurity(http: HttpSecurity): SecurityFilterChain {
+        // @formatter:off
+        http
+            .authorizeHttpRequests({ requests -> requests
+                .anyRequest().authenticated()
+            })
+        return http.build()
+        // @formatter:on
+    }
+
+    @Bean
+    @Order(Ordered.LOWEST_PRECEDENCE)  // <2>
+    fun userAuthorization(): ThrowingCustomizer<HttpSecurity> {
+        // @formatter:off
+        return ThrowingCustomizer { http -> http
+            .authorizeHttpRequests { requests -> requests
+                .requestMatchers("/users/**").hasRole("USER")
+            }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    @Order(Ordered.HIGHEST_PRECEDENCE) // <1>
+    fun adminAuthorization(): ThrowingCustomizer<HttpSecurity> {
+        // @formatter:off
+        return ThrowingCustomizer { http -> http
+            .authorizeHttpRequests { requests -> requests
+                .requestMatchers("/admins/**").hasRole("ADMIN")
+            }
+        }
+        // @formatter:on
+    }
+
+    // <3>
+
+    @Bean
+    fun contentSecurityPolicy(): Customizer<HeadersConfigurer<HttpSecurity>> {
+        // @formatter:off
+        return Customizer { headers -> headers
+            .contentSecurityPolicy { csp -> csp
+                .policyDirectives("object-src 'none'")
+            }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    fun contentTypeOptions(): Customizer<HeadersConfigurer<HttpSecurity>> {
+        // @formatter:off
+        return Customizer { headers -> headers
+            .contentTypeOptions(Customizer.withDefaults())
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    fun httpsRedirect(): Customizer<HttpsRedirectConfigurer<HttpSecurity>> {
+        // @formatter:off
+        return Customizer.withDefaults<HttpsRedirectConfigurer<HttpSecurity>>()
+        // @formatter:on
+    }
+    // end::sample[]
+}

+ 72 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/customizerbeanordering/CustomizerBeanOrderingTests.kt

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kt.docs.servlet.configuration.customizerbeanordering
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class CustomizerBeanOrderingTests {
+    @JvmField
+    val spring = SpringTestContext(this).mockMvcAfterSpringSecurityOk()
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun authorizationOrdered() {
+        this.spring.register(CustomizerBeanOrderingConfiguration::class.java).autowire()
+        // @formatter:off
+        this.mockMvc.get("https://localhost/admins/1") {
+            with(user("admin").roles("ADMIN"))
+        }.andExpect {
+            status { isOk() }
+        }
+        this.mockMvc.get("https://localhost/admins/1") {
+            with(user("user").roles("USER"))
+        }.andExpect {
+            status { isForbidden() }
+        }
+        this.mockMvc.get("https://localhost/users/1") {
+            with(user("user").roles("USER"))
+        }.andExpect {
+            status { isOk() }
+        }
+        this.mockMvc.get("https://localhost/users/1") {
+            with(user("noUserRole").roles("OTHER"))
+        }.andExpect {
+            status { isForbidden() }
+        }
+        this.mockMvc.get("https://localhost/other") {
+            with(user("authenticated").roles("OTHER"))
+        }.andExpect {
+            status { isOk() }
+        }
+        // @formatter:on
+    }
+}

+ 110 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingConfiguration.kt

@@ -0,0 +1,110 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kt.docs.servlet.configuration.dslbeanordering
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.Ordered
+import org.springframework.core.annotation.Order
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.ThrowingCustomizer
+import org.springframework.security.config.annotation.web.HeadersDsl
+import org.springframework.security.config.annotation.web.HttpSecurityDsl
+import org.springframework.security.config.annotation.web.HttpsRedirectDsl
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer
+import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer
+import org.springframework.security.config.annotation.web.invoke
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+/**
+ *
+ */
+@EnableWebMvc
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+internal class DslBeanOrderingConfiguration {
+    // tag::sample[]
+    // All of the Java Modular Configuration is applied first <1>
+
+    @Bean // <5>
+    fun springSecurity(http: HttpSecurity): SecurityFilterChain {
+        // @formatter:off
+        http {
+            authorizeHttpRequests {
+                authorize(anyRequest, authenticated)
+            }
+        }
+        return http.build()
+        // @formatter:on
+    }
+
+    @Bean
+    @Order(Ordered.LOWEST_PRECEDENCE)  // <3>
+    fun userAuthorization(): HttpSecurityDsl.() -> Unit {
+        // @formatter:off
+        return {
+            authorizeHttpRequests {
+                authorize("/users/**", hasRole("USER"))
+            }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    @Order(Ordered.HIGHEST_PRECEDENCE) // <2>
+    fun adminAuthorization(): HttpSecurityDsl.() -> Unit {
+        // @formatter:off
+        return {
+            authorizeHttpRequests {
+                authorize("/admins/**", hasRole("ADMIN"))
+            }
+        }
+        // @formatter:on
+    }
+
+    // <4>
+
+    @Bean
+    fun contentSecurityPolicy(): HeadersDsl.() -> Unit {
+        // @formatter:off
+        return {
+            contentSecurityPolicy {
+                policyDirectives = "object-src 'none'"
+            }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    fun contentTypeOptions(): HeadersDsl.() -> Unit {
+        // @formatter:off
+        return {
+            contentTypeOptions { }
+        }
+        // @formatter:on
+    }
+
+    @Bean
+    fun httpsRedirect(): HttpsRedirectDsl.() -> Unit {
+        // @formatter:off
+        return { }
+        // @formatter:on
+    }
+    // end::sample[]
+}

+ 70 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/dslbeanordering/DslBeanOrderingTests.kt

@@ -0,0 +1,70 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kt.docs.servlet.configuration.dslbeanordering
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.kt.docs.servlet.configuration.customizerbeanordering.CustomizerBeanOrderingConfiguration
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+
+/**
+ *
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class DslBeanOrderingTests {
+    @JvmField
+    val spring = SpringTestContext(this).mockMvcAfterSpringSecurityOk()
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun dslOrdered() {
+        this.spring.register(DslBeanOrderingConfiguration::class.java).autowire()
+        // @formatter:off
+        this.mockMvc.get("https://localhost/admins/1") {
+            with(user("admin").roles("ADMIN"))
+        }.andExpect {
+            status { isOk() }
+        }
+        this.mockMvc.get("https://localhost/admins/1") {
+            with(user("user").roles("USER"))
+        }.andExpect {
+            status { isForbidden() }
+        }
+        this.mockMvc.get("https://localhost/users/1") {
+            with(user("user").roles("USER"))
+        }.andExpect {
+            status { isOk() }
+        }
+        this.mockMvc.get("https://localhost/users/1") {
+            with(user("noUserRole").roles("OTHER"))
+        }.andExpect {
+            status { isForbidden() }
+        }
+        this.mockMvc.get("https://localhost/other") {
+            with(user("authenticated").roles("OTHER"))
+        }.andExpect {
+            status { isOk() }
+        }
+        // @formatter:on
+    }
+}

+ 46 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanConfiguration.kt

@@ -0,0 +1,46 @@
+package org.springframework.security.kt.docs.servlet.configuration.httpsecuritycustomizerbean
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.ThrowingCustomizer
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer
+import org.springframework.security.config.annotation.web.invoke
+import org.springframework.security.web.SecurityFilterChain
+
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+class HttpSecurityCustomizerBeanConfiguration {
+
+    @Bean
+    fun springSecurity(http: HttpSecurity): SecurityFilterChain {
+        http {
+            authorizeHttpRequests {
+                authorize(anyRequest, authenticated)
+            }
+        }
+        return http.build()
+    }
+
+
+    // tag::httpSecurityCustomizer[]
+    @Bean
+    fun httpSecurityCustomizer(): ThrowingCustomizer<HttpSecurity> {
+        // @formatter:off
+        return ThrowingCustomizer { http -> http
+            .headers { headers -> headers
+                .contentSecurityPolicy { csp -> csp
+                    // <1>
+                    .policyDirectives("object-src 'none'")
+                }
+            }
+            // <2>
+            .redirectToHttps(Customizer.withDefaults())
+        }
+        // @formatter:on
+    }
+    // end::httpSecurityCustomizer[]
+
+}

+ 35 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritycustomizerbean/HttpSecurityCustomizerBeanTests.kt

@@ -0,0 +1,35 @@
+package org.springframework.security.kt.docs.servlet.configuration.httpsecuritycustomizerbean
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+
+@ExtendWith(SpringTestContextExtension::class)
+class HttpSecurityCustomizerBeanTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun `httpsecurity customizer config`() {
+        this.spring.register(HttpSecurityCustomizerBeanConfiguration::class.java).autowire()
+
+        this.mockMvc.get("/")
+            .andExpect {
+                redirectedUrl("https://localhost/")
+            }
+        this.mockMvc.get("https://localhost/")
+            .andExpect {
+                header {
+                    string("Content-Security-Policy", "object-src 'none'")
+                }
+            }
+    }
+
+}

+ 52 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanConfiguration.kt

@@ -0,0 +1,52 @@
+package org.springframework.security.kt.docs.servlet.configuration.httpsecuritydslbean
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.HeadersDsl
+import org.springframework.security.config.annotation.web.HttpSecurityDsl
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.invoke
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+
+@EnableWebMvc
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+class HttpSecurityDslBeanConfiguration {
+
+    @Bean
+    fun springSecurity(http: HttpSecurity): SecurityFilterChain {
+        http {
+            authorizeHttpRequests {
+                authorize(anyRequest, authenticated)
+            }
+        }
+        return http.build()
+    }
+
+    // tag::httpSecurityDslBean[]
+    @Bean
+    fun httpSecurityDslBean(): HttpSecurityDsl.() -> Unit {
+        return {
+            headers {
+                contentSecurityPolicy {
+                    // <1>
+                    policyDirectives = "object-src 'none'"
+                }
+            }
+            // <2>
+            redirectToHttps { }
+        }
+    }
+    // end::httpSecurityDslBean[]
+
+}

+ 36 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/httpsecuritydslbean/HttpSecurityDslBeanTests.kt

@@ -0,0 +1,36 @@
+package org.springframework.security.kt.docs.servlet.configuration.httpsecuritydslbean
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+
+
+@ExtendWith(SpringTestContextExtension::class)
+class HttpSecurityDslBeanTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun `HttpSecurityDslBean`() {
+        this.spring.register(HttpSecurityDslBeanConfiguration::class.java).autowire()
+
+        this.mockMvc.get("/")
+            .andExpect {
+                redirectedUrl("https://localhost/")
+            }
+
+        this.mockMvc.get("https://localhost/")
+            .andExpect {
+                header {
+                    string("Content-Security-Policy", "object-src 'none'")
+                }
+            }
+    }
+}

+ 42 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanConfiguration.kt

@@ -0,0 +1,42 @@
+package org.springframework.security.kt.docs.servlet.configuration.toplevelcustomizerbean
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer
+import org.springframework.security.config.annotation.web.invoke
+import org.springframework.security.web.SecurityFilterChain
+
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+class TopLevelCustomizerBeanConfiguration {
+
+    @Bean
+    fun springSecurity(http: HttpSecurity): SecurityFilterChain {
+        // @formatter:off
+        http {
+            authorizeHttpRequests {
+                authorize(anyRequest, authenticated)
+            }
+        }
+        return http.build()
+        // @formatter:on
+    }
+
+    // tag::headersCustomizer[]
+    @Bean
+    fun headersSecurity(): Customizer<HeadersConfigurer<HttpSecurity>> {
+        // @formatter:off
+        return Customizer { headers -> headers
+            .contentSecurityPolicy { csp -> csp
+                // <1>
+                .policyDirectives("object-src 'none'")
+            }
+        }
+        // @formatter:on
+    }
+    // end::headersCustomizer[]
+
+}

+ 31 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/toplevelcustomizerbean/TopLevelCustomizerBeanTests.kt

@@ -0,0 +1,31 @@
+package org.springframework.security.kt.docs.servlet.configuration.toplevelcustomizerbean
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+
+@ExtendWith(SpringTestContextExtension::class)
+class TopLevelCustomizerBeanTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun `top level dsl bean`() {
+        this.spring.register(TopLevelCustomizerBeanConfiguration::class.java).autowire()
+
+        this.mockMvc.get("/")
+            .andExpect {
+                header {
+                    string("Content-Security-Policy", "object-src 'none'")
+                }
+            }
+    }
+
+}

+ 40 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanConfiguration.kt

@@ -0,0 +1,40 @@
+package org.springframework.security.kt.docs.servlet.configuration.topleveldslbean
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.HeadersDsl
+import org.springframework.security.config.annotation.web.HttpSecurityDsl
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.invoke
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+
+@EnableWebMvc
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+class TopLevelDslBeanConfiguration {
+
+    @Bean
+    fun springSecurity(http: HttpSecurity): SecurityFilterChain {
+        http {
+            authorizeHttpRequests {
+                authorize(anyRequest, authenticated)
+            }
+        }
+        return http.build()
+    }
+
+    // tag::headersSecurity[]
+    @Bean
+    fun headersSecurity(): HeadersDsl.() -> Unit {
+        return {
+            contentSecurityPolicy {
+                // <1>
+                policyDirectives = "object-src 'none'"
+            }
+        }
+    }
+    // end::headersSecurity[]
+}

+ 31 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/configuration/topleveldslbean/TopLevelDslBeanTests.kt

@@ -0,0 +1,31 @@
+package org.springframework.security.kt.docs.servlet.configuration.topleveldslbean
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+
+
+@ExtendWith(SpringTestContextExtension::class)
+class TopLevelDslBeanTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun `HttpSecurityDslBean`() {
+        this.spring.register(TopLevelDslBeanConfiguration::class.java).autowire()
+
+        this.mockMvc.get("/")
+            .andExpect {
+                header {
+                    string("Content-Security-Policy", "object-src 'none'")
+                }
+            }
+    }
+}