Skip to content

Add PathPatternRequestMatcher #16499

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
Expand Down Expand Up @@ -43,6 +43,7 @@
import org.springframework.security.config.annotation.web.ServletRegistrationsSupport.RegistrationMapping;
import org.springframework.security.config.annotation.web.configurers.AbstractConfigAttributeRequestMatcherRegistry;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
Expand Down Expand Up @@ -218,10 +219,9 @@ public C requestMatchers(HttpMethod method, String... patterns) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
}
List<RequestMatcher> matchers = new ArrayList<>();
MethodPathRequestMatcherFactory requestMatcherFactory = getRequestMatcherFactory();
for (String pattern : patterns) {
AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null);
MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0);
matchers.add(new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant));
matchers.add(requestMatcherFactory.matcher(method, pattern));
}
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
}
Expand Down Expand Up @@ -264,11 +264,13 @@ private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
}

private static String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. "
+ "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); "
+ "otherwise, please use requestMatchers(AntPathRequestMatcher).\n\n"
+ "This is because there is more than one mappable servlet in your servlet context: %s.\n\n"
+ "For each MvcRequestMatcher, call MvcRequestMatcher#setServletPath to indicate the servlet path.";
String template = """
This method cannot decide whether these patterns are Spring MVC patterns or not. \
This is because there is more than one mappable servlet in your servlet context: %s.

To address this, please create one PathPatternRequestMatcher#servletPath for each servlet that has \
authorized endpoints and use them to construct request matchers manually.
""";
Map<String, Collection<String>> mappings = new LinkedHashMap<>();
for (ServletRegistration registration : registrations) {
mappings.put(registration.getClassName(), registration.getMappings());
Expand Down Expand Up @@ -329,6 +331,13 @@ public C requestMatchers(HttpMethod method) {
*/
protected abstract C chainRequestMatchers(List<RequestMatcher> requestMatchers);

private MethodPathRequestMatcherFactory getRequestMatcherFactory() {
PathPatternRequestMatcher.Builder builder = this.context
.getBeanProvider(PathPatternRequestMatcher.Builder.class)
.getIfUnique();
return (builder != null) ? builder::matcher : new DefaultMethodPathRequestMatcherFactory();
}

/**
* Utilities for creating {@link RequestMatcher} instances.
*
Expand Down Expand Up @@ -402,6 +411,17 @@ static List<RequestMatcher> regexMatchers(String... regexPatterns) {

}

class DefaultMethodPathRequestMatcherFactory implements MethodPathRequestMatcherFactory {

@Override
public RequestMatcher matcher(HttpMethod method, String path) {
AntPathRequestMatcher ant = new AntPathRequestMatcher(path, (method != null) ? method.name() : null);
MvcRequestMatcher mvc = createMvcMatchers(method, path).get(0);
return new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant);
}

}

static class DeferredRequestMatcher implements RequestMatcher {

final Function<ServletContext, RequestMatcher> requestMatcherFactory;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2002-2025 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;

import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

interface MethodPathRequestMatcherFactory {

default RequestMatcher matcher(String path) {
return matcher(null, path);
}

RequestMatcher matcher(HttpMethod method, String path);

static MethodPathRequestMatcherFactory fromApplicationContext(ApplicationContext context) {
PathPatternRequestMatcher.Builder builder = context.getBeanProvider(PathPatternRequestMatcher.Builder.class)
.getIfUnique();
return (builder != null) ? builder::matcher : AntPathRequestMatcher::antMatcher;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
Expand Down Expand Up @@ -90,6 +90,7 @@
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
Expand Down Expand Up @@ -3684,11 +3685,16 @@ public HttpSecurity securityMatcher(RequestMatcher requestMatcher) {
* @see MvcRequestMatcher
*/
public HttpSecurity securityMatcher(String... patterns) {
if (mvcPresent) {
this.requestMatcher = new OrRequestMatcher(createMvcMatchers(patterns));
return this;
List<RequestMatcher> matchers = new ArrayList<>();
PathPatternRequestMatcher.Builder builder = getSharedObject(ApplicationContext.class)
.getBeanProvider(PathPatternRequestMatcher.Builder.class)
.getIfUnique();
MethodPathRequestMatcherFactory factory = (builder != null) ? builder::matcher
: (method, pattern) -> mvcPresent ? createMvcMatcher(pattern) : createAntMatcher(pattern);
for (String pattern : patterns) {
matchers.add(factory.matcher(pattern));
}
this.requestMatcher = new OrRequestMatcher(createAntMatchers(patterns));
this.requestMatcher = new OrRequestMatcher(matchers);
return this;
}

Expand Down Expand Up @@ -3717,15 +3723,11 @@ public HttpSecurity webAuthn(Customizer<WebAuthnConfigurer<HttpSecurity>> webAut
return HttpSecurity.this;
}

private List<RequestMatcher> createAntMatchers(String... patterns) {
List<RequestMatcher> matchers = new ArrayList<>(patterns.length);
for (String pattern : patterns) {
matchers.add(new AntPathRequestMatcher(pattern));
}
return matchers;
private RequestMatcher createAntMatcher(String pattern) {
return new AntPathRequestMatcher(pattern);
}

private List<RequestMatcher> createMvcMatchers(String... mvcPatterns) {
private RequestMatcher createMvcMatcher(String mvcPattern) {
ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, Object.class);
ObjectProvider<ObjectPostProcessor<Object>> postProcessors = getContext().getBeanProvider(type);
ObjectPostProcessor<Object> opp = postProcessors.getObject();
Expand All @@ -3736,13 +3738,9 @@ private List<RequestMatcher> createMvcMatchers(String... mvcPatterns) {
}
HandlerMappingIntrospector introspector = getContext().getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME,
HandlerMappingIntrospector.class);
List<RequestMatcher> matchers = new ArrayList<>(mvcPatterns.length);
for (String mvcPattern : mvcPatterns) {
MvcRequestMatcher matcher = new MvcRequestMatcher(introspector, mvcPattern);
opp.postProcess(matcher);
matchers.add(matcher);
}
return matchers;
MvcRequestMatcher matcher = new MvcRequestMatcher(introspector, mvcPattern);
opp.postProcess(matcher);
return matcher;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2002-2025 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.builders;

import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

interface MethodPathRequestMatcherFactory {

default RequestMatcher matcher(String path) {
return matcher(null, path);
}

RequestMatcher matcher(HttpMethod method, String path);

static MethodPathRequestMatcherFactory fromApplicationContext(ApplicationContext context) {
PathPatternRequestMatcher.Builder builder = context.getBeanProvider(PathPatternRequestMatcher.Builder.class)
.getIfUnique();
return (builder != null) ? builder::matcher : AntPathRequestMatcher::antMatcher;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.security.config.annotation.web.configurers;

import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
Expand All @@ -26,7 +28,6 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

/**
Expand Down Expand Up @@ -234,7 +235,7 @@ public void init(H http) throws Exception {

@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, "POST");
return getRequestMatcherFactory().matcher(HttpMethod.POST, loginProcessingUrl);
}

/**
Expand Down Expand Up @@ -271,4 +272,9 @@ private void initDefaultLoginFilter(H http) {
}
}

private MethodPathRequestMatcherFactory getRequestMatcherFactory() {
return MethodPathRequestMatcherFactory
.fromApplicationContext(getBuilder().getSharedObject(ApplicationContext.class));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

import jakarta.servlet.http.HttpSession;

import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.SecurityConfigurer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -37,7 +39,6 @@
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -368,7 +369,12 @@ private RequestMatcher createLogoutRequestMatcher(H http) {
}

private RequestMatcher createLogoutRequestMatcher(String httpMethod) {
return new AntPathRequestMatcher(this.logoutUrl, httpMethod);
return getRequestMatcherFactory().matcher(HttpMethod.valueOf(httpMethod), this.logoutUrl);
}

private MethodPathRequestMatcherFactory getRequestMatcherFactory() {
return MethodPathRequestMatcherFactory
.fromApplicationContext(getBuilder().getSharedObject(ApplicationContext.class));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2002-2025 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.configurers;

import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

interface MethodPathRequestMatcherFactory {

default RequestMatcher matcher(String path) {
return matcher(null, path);
}

RequestMatcher matcher(HttpMethod method, String path);

static MethodPathRequestMatcherFactory fromApplicationContext(ApplicationContext context) {
PathPatternRequestMatcher.Builder builder = context.getBeanProvider(PathPatternRequestMatcher.Builder.class)
.getIfUnique();
return (builder != null) ? builder::matcher : AntPathRequestMatcher::antMatcher;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

package org.springframework.security.config.annotation.web.configurers;

import org.springframework.context.ApplicationContext;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.web.RequestMatcherRedirectFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

/**
Expand Down Expand Up @@ -55,8 +55,13 @@ public PasswordManagementConfigurer<B> changePasswordPage(String changePasswordP
@Override
public void configure(B http) throws Exception {
RequestMatcherRedirectFilter changePasswordFilter = new RequestMatcherRedirectFilter(
new AntPathRequestMatcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN), this.changePasswordPage);
getRequestMatcherFactory().matcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN), this.changePasswordPage);
http.addFilterBefore(postProcess(changePasswordFilter), UsernamePasswordAuthenticationFilter.class);
}

private MethodPathRequestMatcherFactory getRequestMatcherFactory() {
return MethodPathRequestMatcherFactory
.fromApplicationContext(getBuilder().getSharedObject(ApplicationContext.class));
}

}
Loading