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
This commit is contained in:
Josh Cummings 2025-08-18 06:31:31 -06:00
parent 006f638c0a
commit aeb2dbc2b6
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
10 changed files with 119 additions and 51 deletions

View File

@ -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<C> {
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<C> {
+ "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<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : patterns) {
matchers.add(builder.matcher(method, pattern));
@ -148,6 +152,23 @@ public abstract class AbstractRequestMatcherRegistry<C> {
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("/")) {

View File

@ -2058,7 +2058,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
*/
public HttpSecurity securityMatcher(String... patterns) {
List<RequestMatcher> 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));
}

View File

@ -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();
}
}

View File

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

View File

@ -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<Class<?>, 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;

View File

@ -39,8 +39,6 @@ public abstract class AbstractHttpConfigurer<T extends AbstractHttpConfigurer<T,
private SecurityContextHolderStrategy securityContextHolderStrategy;
private PathPatternRequestMatcher.Builder requestMatcherBuilder;
/**
* Disables the {@link AbstractHttpConfigurer} by removing it. After doing so a fresh
* version of the configuration can be applied.
@ -69,12 +67,7 @@ public abstract class AbstractHttpConfigurer<T extends AbstractHttpConfigurer<T,
}
protected PathPatternRequestMatcher.Builder getRequestMatcherBuilder() {
if (this.requestMatcherBuilder != null) {
return this.requestMatcherBuilder;
}
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
this.requestMatcherBuilder = context.getBean(PathPatternRequestMatcher.Builder.class);
return this.requestMatcherBuilder;
return getBuilder().getSharedObject(PathPatternRequestMatcher.Builder.class);
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.security.config.annotation.web;
import java.util.List;
import java.util.stream.Stream;
import jakarta.servlet.DispatcherType;
import org.junit.jupiter.api.BeforeEach;
@ -68,8 +69,8 @@ public class AbstractRequestMatcherRegistryTests {
ObjectProvider<ObjectPostProcessor<Object>> 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<T> implements ObjectProvider<T> {
private final T object;
private SingleObjectProvider(T object) {
this.object = object;
}
@Override
public Stream<T> stream() {
return Stream.of(this.object);
}
}
}

View File

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

View File

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

View File

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