From aeb2dbc2b60cb3dfdbcbf9cf6024cdcc4a97fde3 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Mon, 18 Aug 2025 06:31:31 -0600 Subject: [PATCH] Move PathPatternRequestMatcher.Builder to Shared Object This commit changes the DSL to look for a shared object instead of publishing a bean for PathPatternRequestMatcher.Builder. Closes gh-17746 --- .../web/AbstractRequestMatcherRegistry.java | 23 +++++++++++- .../annotation/web/builders/HttpSecurity.java | 2 +- .../AuthorizationConfiguration.java | 31 ---------------- .../web/configuration/EnableWebSecurity.java | 2 +- .../HttpSecurityConfiguration.java | 12 +++++++ .../configurers/AbstractHttpConfigurer.java | 9 +---- .../AbstractRequestMatcherRegistryTests.java | 20 +++++++++-- .../AuthorizeHttpRequestsConfigurerTests.java | 10 +++++- .../HttpSecurityRequestMatchersTests.java | 36 +++++++++++++++++++ .../HttpSecuritySecurityMatchersTests.java | 25 +++++++++---- 10 files changed, 119 insertions(+), 51 deletions(-) delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configuration/AuthorizationConfiguration.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index 3ff20ebcf8..2b77896291 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -27,11 +27,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.util.function.ThrowingSupplier; /** * A base class for registering {@link RequestMatcher}'s. For example, it might allow for @@ -52,6 +54,8 @@ public abstract class AbstractRequestMatcherRegistry { private final Log logger = LogFactory.getLog(getClass()); + private PathPatternRequestMatcher.Builder requestMatcherBuilder; + protected final void setApplicationContext(ApplicationContext context) { this.context = context; } @@ -140,7 +144,7 @@ public abstract class AbstractRequestMatcherRegistry { + "Spring Security, leaving out the leading slash will result in an exception."); } Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest"); - PathPatternRequestMatcher.Builder builder = this.context.getBean(PathPatternRequestMatcher.Builder.class); + PathPatternRequestMatcher.Builder builder = getRequestMatcherBuilder(); List matchers = new ArrayList<>(); for (String pattern : patterns) { matchers.add(builder.matcher(method, pattern)); @@ -148,6 +152,23 @@ public abstract class AbstractRequestMatcherRegistry { return requestMatchers(matchers.toArray(new RequestMatcher[0])); } + private PathPatternRequestMatcher.Builder getRequestMatcherBuilder() { + if (this.requestMatcherBuilder != null) { + return this.requestMatcherBuilder; + } + this.requestMatcherBuilder = this.context.getBeanProvider(PathPatternRequestMatcher.Builder.class) + .getIfUnique(() -> constructRequestMatcherBuilder(this.context)); + return this.requestMatcherBuilder; + } + + private PathPatternRequestMatcher.Builder constructRequestMatcherBuilder(ApplicationContext context) { + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder = new PathPatternRequestMatcherBuilderFactoryBean(); + requestMatcherBuilder.setApplicationContext(context); + requestMatcherBuilder.setBeanFactory(context.getAutowireCapableBeanFactory()); + requestMatcherBuilder.setBeanName(requestMatcherBuilder.toString()); + return ThrowingSupplier.of(requestMatcherBuilder::getObject).get(); + } + private boolean anyPathsDontStartWithLeadingSlash(String... patterns) { for (String pattern : patterns) { if (!pattern.startsWith("/")) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index e59d438193..5a694abc6d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -2058,7 +2058,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder matchers = new ArrayList<>(); - PathPatternRequestMatcher.Builder builder = getContext().getBean(PathPatternRequestMatcher.Builder.class); + PathPatternRequestMatcher.Builder builder = getSharedObject(PathPatternRequestMatcher.Builder.class); for (String pattern : patterns) { matchers.add(builder.matcher(pattern)); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/AuthorizationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/AuthorizationConfiguration.java deleted file mode 100644 index aaa7f0a61f..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/AuthorizationConfiguration.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.annotation.web.configuration; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Fallback; -import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; - -class AuthorizationConfiguration { - - @Bean - @Fallback - PathPatternRequestMatcherBuilderFactoryBean pathPatternRequestMatcherBuilder() { - return new PathPatternRequestMatcherBuilderFactoryBean(); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java index 7088f59d21..2a8e07dfda 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java @@ -83,7 +83,7 @@ import org.springframework.security.web.SecurityFilterChain; @Target(ElementType.TYPE) @Documented @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class, - HttpSecurityConfiguration.class, ObservationImportSelector.class, AuthorizationConfiguration.class }) + HttpSecurityConfiguration.class, ObservationImportSelector.class }) @EnableGlobalAuthentication public @interface EnableWebSecurity { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 1a372d6a16..934d1b0559 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -38,12 +38,15 @@ import org.springframework.security.config.annotation.authentication.configurers 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.DefaultLoginPageConfigurer; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.userdetails.UserDetailsService; 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.function.ThrowingSupplier; import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.accept.HeaderContentNegotiationStrategy; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -161,9 +164,18 @@ class HttpSecurityConfiguration { Map, Object> sharedObjects = new HashMap<>(); sharedObjects.put(ApplicationContext.class, this.context); sharedObjects.put(ContentNegotiationStrategy.class, this.contentNegotiationStrategy); + sharedObjects.put(PathPatternRequestMatcher.Builder.class, constructRequestMatcherBuilder(this.context)); return sharedObjects; } + private PathPatternRequestMatcher.Builder constructRequestMatcherBuilder(ApplicationContext context) { + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder = new PathPatternRequestMatcherBuilderFactoryBean(); + requestMatcherBuilder.setApplicationContext(context); + requestMatcherBuilder.setBeanFactory(context.getAutowireCapableBeanFactory()); + requestMatcherBuilder.setBeanName(requestMatcherBuilder.toString()); + return ThrowingSupplier.of(requestMatcherBuilder::getObject).get(); + } + static class DefaultPasswordEncoderAuthenticationManagerBuilder extends AuthenticationManagerBuilder { private PasswordEncoder defaultPasswordEncoder; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java index 4b3b52e70d..3c77cbbbcd 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java @@ -39,8 +39,6 @@ public abstract class AbstractHttpConfigurer> given = this.context.getBeanProvider(type); given(given).willReturn(postProcessors); given(postProcessors.getObject()).willReturn(NO_OP_OBJECT_POST_PROCESSOR); - given(this.context.getBean(PathPatternRequestMatcher.Builder.class)) - .willReturn(PathPatternRequestMatcher.withDefaults()); + given(this.context.getBeanProvider(PathPatternRequestMatcher.Builder.class)) + .willReturn(new SingleObjectProvider<>(PathPatternRequestMatcher.withDefaults())); this.matcherRegistry.setApplicationContext(this.context); } @@ -165,4 +166,19 @@ public class AbstractRequestMatcherRegistryTests { } + private static final class SingleObjectProvider implements ObjectProvider { + + private final T object; + + private SingleObjectProvider(T object) { + this.object = object; + } + + @Override + public Stream stream() { + return Stream.of(this.object); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index 16a9831393..eaaf268eeb 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -54,6 +54,7 @@ import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -1051,12 +1052,19 @@ public class AuthorizeHttpRequestsConfigurerTests { @EnableWebSecurity static class ServletPathConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requesMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + @Bean SecurityFilterChain filterChain(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { // @formatter:off return http .authorizeHttpRequests((authorize) -> authorize - .requestMatchers(builder.basePath("/spring").matcher("/")).hasRole("ADMIN") + .requestMatchers(builder.matcher("/")).hasRole("ADMIN") ) .build(); // @formatter:on diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java index 3727edc98d..1b710200a8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java @@ -32,6 +32,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.FilterChainProxy; @@ -157,6 +158,11 @@ public class HttpSecurityRequestMatchersTests { @EnableWebMvc static class MultiMvcMatcherInLambdaConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + @Bean @Order(Ordered.HIGHEST_PRECEDENCE) SecurityFilterChain first(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { @@ -204,6 +210,11 @@ public class HttpSecurityRequestMatchersTests { @EnableWebMvc static class MultiMvcMatcherConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + @Bean @Order(Ordered.HIGHEST_PRECEDENCE) SecurityFilterChain first(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { @@ -249,6 +260,11 @@ public class HttpSecurityRequestMatchersTests { @EnableWebMvc static class MvcMatcherConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + @Bean SecurityFilterChain filterChain(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { // @formatter:off @@ -283,6 +299,11 @@ public class HttpSecurityRequestMatchersTests { @EnableWebMvc static class RequestMatchersMvcMatcherConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + @Bean SecurityFilterChain filterChain(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { // @formatter:off @@ -318,6 +339,11 @@ public class HttpSecurityRequestMatchersTests { @EnableWebMvc static class RequestMatchersMvcMatcherInLambdaConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + @Bean SecurityFilterChain filterChain(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { // @formatter:off @@ -350,6 +376,11 @@ public class HttpSecurityRequestMatchersTests { @EnableWebMvc static class RequestMatchersMvcMatcherServeltPathConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + @Bean SecurityFilterChain filterChain(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { // @formatter:off @@ -386,6 +417,11 @@ public class HttpSecurityRequestMatchersTests { @EnableWebMvc static class RequestMatchersMvcMatcherServletPathInLambdaConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + @Bean SecurityFilterChain filterChain(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { // @formatter:off diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java index 7eb9743d21..5502a6c1ab 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java @@ -32,6 +32,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -354,14 +355,20 @@ public class HttpSecuritySecurityMatchersTests { @Import(UsersConfig.class) static class SecurityMatchersMvcMatcherServletPathConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + @Bean SecurityFilterChain appSecurity(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { - PathPatternRequestMatcher.Builder spring = builder.basePath("/spring"); // @formatter:off http .securityMatchers((security) -> security - .requestMatchers(spring.matcher("/path")) - .requestMatchers(spring.matcher("/never-match")) + .requestMatchers(builder.matcher("/path")) + .requestMatchers(builder.matcher("/never-match")) ) .httpBasic(withDefaults()) .authorizeHttpRequests((authorize) -> authorize @@ -388,14 +395,20 @@ public class HttpSecuritySecurityMatchersTests { @Import(UsersConfig.class) static class SecurityMatchersMvcMatcherServletPathInLambdaConfig { + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + @Bean SecurityFilterChain appSecurity(HttpSecurity http, PathPatternRequestMatcher.Builder builder) throws Exception { - PathPatternRequestMatcher.Builder spring = builder.basePath("/spring"); // @formatter:off http .securityMatchers((matchers) -> matchers - .requestMatchers(spring.matcher("/path")) - .requestMatchers(spring.matcher("/never-match")) + .requestMatchers(builder.matcher("/path")) + .requestMatchers(builder.matcher("/never-match")) ) .httpBasic(withDefaults()) .authorizeHttpRequests((authorize) -> authorize