1
0
mirror of synced 2026-08-31 22:46:02 +00:00

Add Modular Spring Security Configuration

Closes gh-16258
This commit is contained in:
Rob Winch
2025-08-20 09:32:34 -05:00
parent 5c5efc9092
commit a8f045eb50
49 changed files with 3207 additions and 0 deletions
@@ -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;
}
@@ -16,19 +16,24 @@
package org.springframework.security.config.annotation.web.configuration;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.function.ThrowingSupplier;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
@@ -131,6 +137,8 @@ class HttpSecurityConfiguration {
// @formatter:on
applyCorsIfAvailable(http);
applyDefaultConfigurers(http);
applyHttpSecurityCustomizers(this.context, http);
applyTopLevelCustomizers(this.context, 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() {
Map<Class<?>, Object> sharedObjects = new HashMap<>();
sharedObjects.put(ApplicationContext.class, this.context);
@@ -16,6 +16,7 @@
package org.springframework.security.config.annotation.web.reactive;
import java.lang.reflect.Modifier;
import java.util.Map;
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.Scope;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.web.server.ServerHttpSecurity;
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.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver;
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.result.method.annotation.ArgumentResolverConfigurer;
@@ -154,6 +159,83 @@ class ServerHttpSecurityConfiguration {
@Bean(HTTPSECURITY_BEAN_NAME)
@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() {
ContextAwareServerHttpSecurity http = new ContextAwareServerHttpSecurity();
// @formatter:off
@@ -1316,6 +1316,10 @@ public class ServerHttpSecurity {
return this.context.getBeanNamesForType(beanClass);
}
ApplicationContext getApplicationContext() {
return this.context;
}
protected void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
@@ -18,14 +18,22 @@ package org.springframework.security.config.annotation.web
import jakarta.servlet.Filter
import jakarta.servlet.http.HttpServletRequest
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.AuthenticationManager
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
import org.springframework.security.web.DefaultSecurityFilterChain
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].
@@ -77,6 +85,117 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
var authenticationManager: AuthenticationManager? = null
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]
*
@@ -16,13 +16,23 @@
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.config.Customizer
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.context.ServerSecurityContextRepository
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.WebFilter
import java.lang.reflect.Method
import java.lang.reflect.Modifier
/**
* 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 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
* provided [ServerWebExchangeMatcher].
@@ -28,14 +28,20 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
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.context.event.EventListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.mock.web.MockHttpSession;
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.configurers.AbstractHttpConfigurer;
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.test.SpringTestContext;
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.UrlBasedCorsConfigurationSource;
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.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
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.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
@@ -425,6 +435,77 @@ public class HttpSecurityConfigurationTests {
.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
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 {
@Override
@@ -29,12 +29,17 @@ import io.micrometer.observation.ObservationRegistry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
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.security.authentication.TestingAuthenticationToken;
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.users.ReactiveAuthenticationTestConfiguration;
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.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -267,6 +273,47 @@ public class ServerHttpSecurityConfigurationTests {
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
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);
}
}
}
@@ -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'"
}
}
}
}
}