mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-02-28 18:39:06 +00:00
Add PathPatterRequestMatcher
Closes gh-16429 Clsoes gh-16430
This commit is contained in:
parent
4f25f0b90f
commit
588220a020
@ -264,11 +264,13 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
private static String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
||||||
String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. "
|
String template = """
|
||||||
+ "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); "
|
This method cannot decide whether these patterns are Spring MVC patterns or not. \
|
||||||
+ "otherwise, please use requestMatchers(AntPathRequestMatcher).\n\n"
|
This is because there is more than one mappable servlet in your servlet context: %s.
|
||||||
+ "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.";
|
To address this, please create one PathPatternRequestMatcher.Builder#servletPath for each servlet that has \
|
||||||
|
authorized endpoints and use them to construct request matchers manually.
|
||||||
|
""";
|
||||||
Map<String, Collection<String>> mappings = new LinkedHashMap<>();
|
Map<String, Collection<String>> mappings = new LinkedHashMap<>();
|
||||||
for (ServletRegistration registration : registrations) {
|
for (ServletRegistration registration : registrations) {
|
||||||
mappings.put(registration.getClassName(), registration.getMappings());
|
mappings.put(registration.getClassName(), registration.getMappings());
|
||||||
|
@ -577,15 +577,11 @@ http {
|
|||||||
======
|
======
|
||||||
|
|
||||||
[[match-by-mvc]]
|
[[match-by-mvc]]
|
||||||
=== Using an MvcRequestMatcher
|
=== Matching by Servlet Path
|
||||||
|
|
||||||
Generally speaking, you can use `requestMatchers(String)` as demonstrated above.
|
Generally speaking, you can use `requestMatchers(String)` as demonstrated above.
|
||||||
|
|
||||||
However, if you map Spring MVC to a different servlet path, then you need to account for that in your security configuration.
|
However, if you have authorization rules from multiple servlets, you need to specify those:
|
||||||
|
|
||||||
For example, if Spring MVC is mapped to `/spring-mvc` instead of `/` (the default), then you may have an endpoint like `/spring-mvc/my/controller` that you want to authorize.
|
|
||||||
|
|
||||||
You need to use `MvcRequestMatcher` to split the servlet path and the controller path in your configuration like so:
|
|
||||||
|
|
||||||
.Match by MvcRequestMatcher
|
.Match by MvcRequestMatcher
|
||||||
[tabs]
|
[tabs]
|
||||||
@ -594,16 +590,15 @@ Java::
|
|||||||
+
|
+
|
||||||
[source,java,role="primary"]
|
[source,java,role="primary"]
|
||||||
----
|
----
|
||||||
@Bean
|
import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.withDefaults;
|
||||||
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
|
|
||||||
return new MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) {
|
SecurityFilterChain appEndpoints(HttpSecurity http) {
|
||||||
|
PathPatternRequestMatcher.Builder mvc = withDefaults().servletPath("/spring-mvc");
|
||||||
http
|
http
|
||||||
.authorizeHttpRequests((authorize) -> authorize
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
.requestMatchers(mvc.pattern("/my/controller/**")).hasAuthority("controller")
|
.requestMatchers(mvc.matcher("/admin/**")).hasAuthority("admin")
|
||||||
|
.requestMatchers(mvc.matcher("/my/controller/**")).hasAuthority("controller")
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -616,17 +611,15 @@ Kotlin::
|
|||||||
[source,kotlin,role="secondary"]
|
[source,kotlin,role="secondary"]
|
||||||
----
|
----
|
||||||
@Bean
|
@Bean
|
||||||
fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
|
fun appEndpoints(http: HttpSecurity): SecurityFilterChain {
|
||||||
MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun appEndpoints(http: HttpSecurity, mvc: MvcRequestMatcher.Builder): SecurityFilterChain =
|
|
||||||
http {
|
http {
|
||||||
authorizeHttpRequests {
|
authorizeHttpRequests {
|
||||||
authorize(mvc.pattern("/my/controller/**"), hasAuthority("controller"))
|
authorize("/spring-mvc", "/admin/**", hasAuthority("admin"))
|
||||||
|
authorize("/spring-mvc", "/my/controller/**", hasAuthority("controller"))
|
||||||
authorize(anyRequest, authenticated)
|
authorize(anyRequest, authenticated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
Xml::
|
Xml::
|
||||||
@ -634,16 +627,19 @@ Xml::
|
|||||||
[source,xml,role="secondary"]
|
[source,xml,role="secondary"]
|
||||||
----
|
----
|
||||||
<http>
|
<http>
|
||||||
|
<intercept-url servlet-path="/spring-mvc" pattern="/admin/**" access="hasAuthority('admin')"/>
|
||||||
<intercept-url servlet-path="/spring-mvc" pattern="/my/controller/**" access="hasAuthority('controller')"/>
|
<intercept-url servlet-path="/spring-mvc" pattern="/my/controller/**" access="hasAuthority('controller')"/>
|
||||||
<intercept-url pattern="/**" access="authenticated"/>
|
<intercept-url pattern="/**" access="authenticated"/>
|
||||||
</http>
|
</http>
|
||||||
----
|
----
|
||||||
======
|
======
|
||||||
|
|
||||||
This need can arise in at least two different ways:
|
This is because Spring Security requires all URIs to be absolute (minus the context path).
|
||||||
|
|
||||||
* If you use the `spring.mvc.servlet.path` Boot property to change the default path (`/`) to something else
|
[TIP]
|
||||||
* If you register more than one Spring MVC `DispatcherServlet` (thus requiring that one of them not be the default path)
|
=====
|
||||||
|
There are several other components that create request matchers for you like {spring-boot-api-url}org/springframework/boot/autoconfigure/security/servlet/PathRequest.html[`PathRequest#toStaticResources#atCommonLocations`]
|
||||||
|
=====
|
||||||
|
|
||||||
[[match-by-custom]]
|
[[match-by-custom]]
|
||||||
=== Using a Custom Matcher
|
=== Using a Custom Matcher
|
||||||
|
@ -46,6 +46,7 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
|
|||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||||
import org.springframework.web.filter.GenericFilterBean;
|
import org.springframework.web.filter.GenericFilterBean;
|
||||||
|
import org.springframework.web.filter.ServletRequestPathFilter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegates {@code Filter} requests to a list of Spring-managed filter beans. As of
|
* Delegates {@code Filter} requests to a list of Spring-managed filter beans. As of
|
||||||
@ -162,6 +163,8 @@ public class FilterChainProxy extends GenericFilterBean {
|
|||||||
|
|
||||||
private FilterChainDecorator filterChainDecorator = new VirtualFilterChainDecorator();
|
private FilterChainDecorator filterChainDecorator = new VirtualFilterChainDecorator();
|
||||||
|
|
||||||
|
private Filter springWebFilter = new ServletRequestPathFilter();
|
||||||
|
|
||||||
public FilterChainProxy() {
|
public FilterChainProxy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,27 +213,29 @@ public class FilterChainProxy extends GenericFilterBean {
|
|||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
|
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
|
||||||
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
|
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
|
||||||
List<Filter> filters = getFilters(firewallRequest);
|
this.springWebFilter.doFilter(firewallRequest, firewallResponse, (r, s) -> {
|
||||||
if (filters == null || filters.isEmpty()) {
|
List<Filter> filters = getFilters(firewallRequest);
|
||||||
if (logger.isTraceEnabled()) {
|
if (filters == null || filters.isEmpty()) {
|
||||||
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
|
if (logger.isTraceEnabled()) {
|
||||||
|
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
|
||||||
|
}
|
||||||
|
firewallRequest.reset();
|
||||||
|
this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
firewallRequest.reset();
|
|
||||||
this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
|
|
||||||
}
|
|
||||||
FilterChain reset = (req, res) -> {
|
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
|
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
|
||||||
}
|
}
|
||||||
// Deactivate path stripping as we exit the security filter chain
|
FilterChain reset = (req, res) -> {
|
||||||
firewallRequest.reset();
|
if (logger.isDebugEnabled()) {
|
||||||
chain.doFilter(req, res);
|
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
|
||||||
};
|
}
|
||||||
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
|
// Deactivate path stripping as we exit the security filter chain
|
||||||
|
firewallRequest.reset();
|
||||||
|
chain.doFilter(req, res);
|
||||||
|
};
|
||||||
|
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -447,4 +452,23 @@ public class FilterChainProxy extends GenericFilterBean {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class FirewallFilter implements Filter {
|
||||||
|
|
||||||
|
private final HttpFirewall firewall;
|
||||||
|
|
||||||
|
private FirewallFilter(HttpFirewall firewall) {
|
||||||
|
this.firewall = firewall;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||||
|
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||||||
|
filterChain.doFilter(this.firewall.getFirewalledRequest(request),
|
||||||
|
this.firewall.getFirewalledResponse(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,370 @@
|
|||||||
|
/*
|
||||||
|
* 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.web.servlet.util.matcher;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletContext;
|
||||||
|
import jakarta.servlet.ServletRegistration;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.server.PathContainer;
|
||||||
|
import org.springframework.http.server.RequestPath;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
|
||||||
|
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.web.util.ServletRequestPathUtils;
|
||||||
|
import org.springframework.web.util.pattern.PathPattern;
|
||||||
|
import org.springframework.web.util.pattern.PathPatternParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RequestMatcher} that uses {@link PathPattern}s to match against each
|
||||||
|
* {@link HttpServletRequest}. The provided path should be relative to the servlet (that
|
||||||
|
* is, it should exclude any context or servlet path).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* To also match the servlet, please see {@link PathPatternRequestMatcher#servletPath}
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Note that the {@link org.springframework.web.servlet.HandlerMapping} that contains the
|
||||||
|
* related URI patterns must be using {@link PathPatternParser#defaultInstance}. If that
|
||||||
|
* is not the case, use {@link PathPatternParser} to parse your path and provide a
|
||||||
|
* {@link PathPattern} in the constructor.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Josh Cummings
|
||||||
|
* @since 6.5
|
||||||
|
*/
|
||||||
|
public final class PathPatternRequestMatcher implements RequestMatcher {
|
||||||
|
|
||||||
|
private final PathPattern pattern;
|
||||||
|
|
||||||
|
private RequestMatcher servletPath = AnyRequestMatcher.INSTANCE;
|
||||||
|
|
||||||
|
private RequestMatcher method = AnyRequestMatcher.INSTANCE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link PathPatternRequestMatcher} that uses the provided {@code pattern}.
|
||||||
|
* <p>
|
||||||
|
* The {@code pattern} should be relative to the servlet path
|
||||||
|
* </p>
|
||||||
|
* @param pattern the pattern used to match
|
||||||
|
*/
|
||||||
|
private PathPatternRequestMatcher(PathPattern pattern) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use {@link PathPatternParser#defaultInstance} to parse path patterns.
|
||||||
|
* @return a {@link Builder} that treats URIs as relative to the context path, if any
|
||||||
|
*/
|
||||||
|
public static Builder withDefaults() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this {@link PathPatternParser} to parse path patterns.
|
||||||
|
* @param parser the {@link PathPatternParser} to use
|
||||||
|
* @return a {@link Builder} that treats URIs as relative to the given
|
||||||
|
* {@code servletPath}
|
||||||
|
*/
|
||||||
|
public static Builder withPathPatternParser(PathPatternParser parser) {
|
||||||
|
Assert.notNull(parser, "pathPatternParser cannot be null");
|
||||||
|
return new Builder(parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean matches(HttpServletRequest request) {
|
||||||
|
return matcher(request).isMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public MatchResult matcher(HttpServletRequest request) {
|
||||||
|
if (!this.servletPath.matches(request)) {
|
||||||
|
return MatchResult.notMatch();
|
||||||
|
}
|
||||||
|
if (!this.method.matches(request)) {
|
||||||
|
return MatchResult.notMatch();
|
||||||
|
}
|
||||||
|
PathContainer path = getRequestPath(request).pathWithinApplication();
|
||||||
|
PathPattern.PathMatchInfo info = this.pattern.matchAndExtract(path);
|
||||||
|
return (info != null) ? MatchResult.match(info.getUriVariables()) : MatchResult.notMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMethod(RequestMatcher method) {
|
||||||
|
this.method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setServletPath(RequestMatcher servletPath) {
|
||||||
|
this.servletPath = servletPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestPath getRequestPath(HttpServletRequest request) {
|
||||||
|
return ServletRequestPathUtils.getParsedRequestPath(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof PathPatternRequestMatcher that)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Objects.equals(this.pattern, that.pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(this.pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder request = new StringBuilder();
|
||||||
|
if (this.method instanceof HttpMethodRequestMatcher m) {
|
||||||
|
request.append(m.method.name()).append(' ');
|
||||||
|
}
|
||||||
|
if (this.servletPath instanceof ServletPathRequestMatcher s) {
|
||||||
|
request.append(s.path);
|
||||||
|
}
|
||||||
|
return "PathPattern [" + request + this.pattern + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for specifying various elements of a request for the purpose of creating
|
||||||
|
* a {@link PathPatternRequestMatcher}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* For example, if Spring MVC is deployed to `/mvc` and another servlet to `/other`,
|
||||||
|
* then you can use this builder to do:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* http
|
||||||
|
* .authorizeHttpRequests((authorize) -> authorize
|
||||||
|
* .requestMatchers(servletPath("/mvc").matcher("/user/**")).hasAuthority("user")
|
||||||
|
* .requestMatchers(servletPath("/other").matcher("/admin/**")).hasAuthority("admin")
|
||||||
|
* )
|
||||||
|
* ...
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
public static final class Builder {
|
||||||
|
|
||||||
|
private final PathPatternParser parser;
|
||||||
|
|
||||||
|
private final RequestMatcher servletPath;
|
||||||
|
|
||||||
|
Builder() {
|
||||||
|
this(PathPatternParser.defaultInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder(PathPatternParser parser) {
|
||||||
|
this(parser, AnyRequestMatcher.INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder(PathPatternParser parser, RequestMatcher servletPath) {
|
||||||
|
this.parser = parser;
|
||||||
|
this.servletPath = servletPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match requests starting with this {@code servletPath}.
|
||||||
|
* @param servletPath the servlet path prefix
|
||||||
|
* @return the {@link Builder} for more configuration
|
||||||
|
*/
|
||||||
|
public Builder servletPath(String servletPath) {
|
||||||
|
return new Builder(this.parser, new ServletPathRequestMatcher(servletPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match requests having this path pattern.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* When the HTTP {@code method} is null, then the matcher does not consider the
|
||||||
|
* HTTP method
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Path patterns always start with a slash and may contain placeholders. They can
|
||||||
|
* also be followed by {@code /**} to signify all URIs under a given path.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* These must be specified relative to any servlet path prefix (meaning you should
|
||||||
|
* exclude the context path and any servlet path prefix in stating your pattern).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The following are valid patterns and their meaning
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code /path} - match exactly and only `/path`</li>
|
||||||
|
* <li>{@code /path/**} - match `/path` and any of its descendents</li>
|
||||||
|
* <li>{@code /path/{value}/**} - match `/path/subdirectory` and any of its
|
||||||
|
* descendents, capturing the value of the subdirectory in
|
||||||
|
* {@link RequestAuthorizationContext#getVariables()}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A more comprehensive list can be found at {@link PathPattern}.
|
||||||
|
* @param path the path pattern to match
|
||||||
|
* @return the {@link Builder} for more configuration
|
||||||
|
*/
|
||||||
|
public PathPatternRequestMatcher matcher(String path) {
|
||||||
|
return matcher(null, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match requests having this {@link HttpMethod} and path pattern.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* When the HTTP {@code method} is null, then the matcher does not consider the
|
||||||
|
* HTTP method
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Path patterns always start with a slash and may contain placeholders. They can
|
||||||
|
* also be followed by {@code /**} to signify all URIs under a given path.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* These must be specified relative to any servlet path prefix (meaning you should
|
||||||
|
* exclude the context path and any servlet path prefix in stating your pattern).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The following are valid patterns and their meaning
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code /path} - match exactly and only `/path`</li>
|
||||||
|
* <li>{@code /path/**} - match `/path` and any of its descendents</li>
|
||||||
|
* <li>{@code /path/{value}/**} - match `/path/subdirectory` and any of its
|
||||||
|
* descendents, capturing the value of the subdirectory in
|
||||||
|
* {@link RequestAuthorizationContext#getVariables()}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A more comprehensive list can be found at {@link PathPattern}.
|
||||||
|
* @param method the {@link HttpMethod} to match, may be null
|
||||||
|
* @param path the path pattern to match
|
||||||
|
* @return the {@link Builder} for more configuration
|
||||||
|
*/
|
||||||
|
public PathPatternRequestMatcher matcher(@Nullable HttpMethod method, String path) {
|
||||||
|
Assert.notNull(path, "pattern cannot be null");
|
||||||
|
Assert.isTrue(path.startsWith("/"), "pattern must start with a /");
|
||||||
|
PathPattern pathPattern = this.parser.parse(path);
|
||||||
|
PathPatternRequestMatcher requestMatcher = new PathPatternRequestMatcher(pathPattern);
|
||||||
|
if (method != null) {
|
||||||
|
requestMatcher.setMethod(new HttpMethodRequestMatcher(method));
|
||||||
|
}
|
||||||
|
if (this.servletPath != AnyRequestMatcher.INSTANCE) {
|
||||||
|
requestMatcher.setServletPath(this.servletPath);
|
||||||
|
}
|
||||||
|
return requestMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class HttpMethodRequestMatcher implements RequestMatcher {
|
||||||
|
|
||||||
|
private final HttpMethod method;
|
||||||
|
|
||||||
|
HttpMethodRequestMatcher(HttpMethod method) {
|
||||||
|
this.method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(HttpServletRequest request) {
|
||||||
|
return this.method.name().equals(request.getMethod());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "HttpMethod [" + this.method + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ServletPathRequestMatcher implements RequestMatcher {
|
||||||
|
|
||||||
|
private final String path;
|
||||||
|
|
||||||
|
private final AtomicReference<Boolean> servletExists = new AtomicReference<>();
|
||||||
|
|
||||||
|
ServletPathRequestMatcher(String servletPath) {
|
||||||
|
Assert.notNull(servletPath, "servletPath cannot be null");
|
||||||
|
Assert.isTrue(servletPath.startsWith("/"), "servletPath must start with '/'");
|
||||||
|
Assert.isTrue(!servletPath.endsWith("/"), "servletPath must not end with a slash");
|
||||||
|
Assert.isTrue(!servletPath.contains("*"), "servletPath must not contain a star");
|
||||||
|
this.path = servletPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(HttpServletRequest request) {
|
||||||
|
Assert.isTrue(servletExists(request), () -> this.path + "/* does not exist in your servlet registration "
|
||||||
|
+ registrationMappings(request));
|
||||||
|
return Objects.equals(this.path, ServletRequestPathUtils.getServletPathPrefix(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean servletExists(HttpServletRequest request) {
|
||||||
|
return this.servletExists.updateAndGet((value) -> {
|
||||||
|
if (value != null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (request.getAttribute("org.springframework.test.web.servlet.MockMvc.MVC_RESULT_ATTRIBUTE") != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (ServletRegistration registration : request.getServletContext()
|
||||||
|
.getServletRegistrations()
|
||||||
|
.values()) {
|
||||||
|
if (registration.getMappings().contains(this.path + "/*")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Collection<String>> registrationMappings(HttpServletRequest request) {
|
||||||
|
Map<String, Collection<String>> map = new LinkedHashMap<>();
|
||||||
|
ServletContext servletContext = request.getServletContext();
|
||||||
|
for (ServletRegistration registration : servletContext.getServletRegistrations().values()) {
|
||||||
|
map.put(registration.getName(), registration.getMappings());
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ServletPath [" + this.path + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -48,6 +48,7 @@ import org.springframework.security.web.firewall.FirewalledRequest;
|
|||||||
import org.springframework.security.web.firewall.HttpFirewall;
|
import org.springframework.security.web.firewall.HttpFirewall;
|
||||||
import org.springframework.security.web.firewall.RequestRejectedException;
|
import org.springframework.security.web.firewall.RequestRejectedException;
|
||||||
import org.springframework.security.web.firewall.RequestRejectedHandler;
|
import org.springframework.security.web.firewall.RequestRejectedHandler;
|
||||||
|
import org.springframework.security.web.servlet.TestMockHttpServletMappings;
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@ -166,6 +167,7 @@ public class FilterChainProxyTests {
|
|||||||
FirewalledRequest fwr = mock(FirewalledRequest.class);
|
FirewalledRequest fwr = mock(FirewalledRequest.class);
|
||||||
given(fwr.getRequestURI()).willReturn("/");
|
given(fwr.getRequestURI()).willReturn("/");
|
||||||
given(fwr.getContextPath()).willReturn("");
|
given(fwr.getContextPath()).willReturn("");
|
||||||
|
given(fwr.getHttpServletMapping()).willReturn(TestMockHttpServletMappings.defaultMapping());
|
||||||
this.fcp.setFirewall(fw);
|
this.fcp.setFirewall(fw);
|
||||||
given(fw.getFirewalledRequest(this.request)).willReturn(fwr);
|
given(fw.getFirewalledRequest(this.request)).willReturn(fwr);
|
||||||
given(this.matcher.matches(any(HttpServletRequest.class))).willReturn(false);
|
given(this.matcher.matches(any(HttpServletRequest.class))).willReturn(false);
|
||||||
@ -183,9 +185,11 @@ public class FilterChainProxyTests {
|
|||||||
FirewalledRequest firstFwr = mock(FirewalledRequest.class, "firstFwr");
|
FirewalledRequest firstFwr = mock(FirewalledRequest.class, "firstFwr");
|
||||||
given(firstFwr.getRequestURI()).willReturn("/");
|
given(firstFwr.getRequestURI()).willReturn("/");
|
||||||
given(firstFwr.getContextPath()).willReturn("");
|
given(firstFwr.getContextPath()).willReturn("");
|
||||||
|
given(firstFwr.getHttpServletMapping()).willReturn(TestMockHttpServletMappings.defaultMapping());
|
||||||
FirewalledRequest fwr = mock(FirewalledRequest.class, "fwr");
|
FirewalledRequest fwr = mock(FirewalledRequest.class, "fwr");
|
||||||
given(fwr.getRequestURI()).willReturn("/");
|
given(fwr.getRequestURI()).willReturn("/");
|
||||||
given(fwr.getContextPath()).willReturn("");
|
given(fwr.getContextPath()).willReturn("");
|
||||||
|
given(fwr.getHttpServletMapping()).willReturn(TestMockHttpServletMappings.defaultMapping());
|
||||||
given(fw.getFirewalledRequest(this.request)).willReturn(firstFwr);
|
given(fw.getFirewalledRequest(this.request)).willReturn(firstFwr);
|
||||||
given(fw.getFirewalledRequest(firstFwr)).willReturn(fwr);
|
given(fw.getFirewalledRequest(firstFwr)).willReturn(fwr);
|
||||||
given(fwr.getRequest()).willReturn(firstFwr);
|
given(fwr.getRequest()).willReturn(firstFwr);
|
||||||
|
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* 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.web.servlet.util.matcher;
|
||||||
|
|
||||||
|
import jakarta.servlet.Servlet;
|
||||||
|
import jakarta.servlet.ServletContext;
|
||||||
|
import jakarta.servlet.ServletRegistration;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.security.web.servlet.MockServletContext;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.web.util.ServletRequestPathUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link PathPatternRequestMatcher}
|
||||||
|
*/
|
||||||
|
public class PathPatternRequestMatcherTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenPatternMatchesRequestThenMatchResult() {
|
||||||
|
RequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher("/uri");
|
||||||
|
assertThat(matcher.matches(request("/uri"))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenPatternContainsPlaceholdersThenMatchResult() {
|
||||||
|
RequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher("/uri/{username}");
|
||||||
|
assertThat(matcher.matcher(request("/uri/bob")).getVariables()).containsEntry("username", "bob");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenOnlyPathInfoMatchesThenMatches() {
|
||||||
|
RequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher("/uri");
|
||||||
|
assertThat(matcher.matches(request("GET", "/mvc/uri", "/mvc"))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenUriContainsServletPathThenNoMatch() {
|
||||||
|
RequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher("/mvc/uri");
|
||||||
|
assertThat(matcher.matches(request("GET", "/mvc/uri", "/mvc"))).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenSameMethodThenMatchResult() {
|
||||||
|
RequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, "/uri");
|
||||||
|
assertThat(matcher.matches(request("/uri"))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenDifferentPathThenNoMatch() {
|
||||||
|
RequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, "/uri");
|
||||||
|
assertThat(matcher.matches(request("GET", "/urj", ""))).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenDifferentMethodThenNoMatch() {
|
||||||
|
RequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, "/uri");
|
||||||
|
assertThat(matcher.matches(request("POST", "/mvc/uri", "/mvc"))).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenNoMethodThenMatches() {
|
||||||
|
RequestMatcher matcher = PathPatternRequestMatcher.withDefaults().matcher("/uri");
|
||||||
|
assertThat(matcher.matches(request("POST", "/uri", ""))).isTrue();
|
||||||
|
assertThat(matcher.matches(request("GET", "/uri", ""))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenServletPathThenMatchesOnlyServletPath() {
|
||||||
|
PathPatternRequestMatcher.Builder servlet = PathPatternRequestMatcher.withDefaults()
|
||||||
|
.servletPath("/servlet/path");
|
||||||
|
RequestMatcher matcher = servlet.matcher(HttpMethod.GET, "/endpoint");
|
||||||
|
ServletContext servletContext = servletContext("/servlet/path");
|
||||||
|
MockHttpServletRequest mock = get("/servlet/path/endpoint").servletPath("/servlet/path")
|
||||||
|
.buildRequest(servletContext);
|
||||||
|
ServletRequestPathUtils.parseAndCache(mock);
|
||||||
|
assertThat(matcher.matches(mock)).isTrue();
|
||||||
|
mock = get("/endpoint").servletPath("/endpoint").buildRequest(servletContext);
|
||||||
|
ServletRequestPathUtils.parseAndCache(mock);
|
||||||
|
assertThat(matcher.matches(mock)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenRequestPathThenIgnoresServletPath() {
|
||||||
|
PathPatternRequestMatcher.Builder request = PathPatternRequestMatcher.withDefaults();
|
||||||
|
RequestMatcher matcher = request.matcher(HttpMethod.GET, "/endpoint");
|
||||||
|
MockHttpServletRequest mock = get("/servlet/path/endpoint").servletPath("/servlet/path").buildRequest(null);
|
||||||
|
ServletRequestPathUtils.parseAndCache(mock);
|
||||||
|
assertThat(matcher.matches(mock)).isTrue();
|
||||||
|
mock = get("/endpoint").servletPath("/endpoint").buildRequest(null);
|
||||||
|
ServletRequestPathUtils.parseAndCache(mock);
|
||||||
|
assertThat(matcher.matches(mock)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matcherWhenServletPathThenRequiresServletPathToExist() {
|
||||||
|
PathPatternRequestMatcher.Builder servlet = PathPatternRequestMatcher.withDefaults()
|
||||||
|
.servletPath("/servlet/path");
|
||||||
|
RequestMatcher matcher = servlet.matcher(HttpMethod.GET, "/endpoint");
|
||||||
|
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(
|
||||||
|
() -> matcher.matches(get("/servlet/path/endpoint").servletPath("/servlet/path").buildRequest(null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void servletPathWhenEndsWithSlashOrStarThenIllegalArgument() {
|
||||||
|
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||||
|
.isThrownBy(() -> PathPatternRequestMatcher.withDefaults().servletPath("/path/**"));
|
||||||
|
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||||
|
.isThrownBy(() -> PathPatternRequestMatcher.withDefaults().servletPath("/path/*"));
|
||||||
|
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||||
|
.isThrownBy(() -> PathPatternRequestMatcher.withDefaults().servletPath("/path/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
MockHttpServletRequest request(String uri) {
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", uri);
|
||||||
|
ServletRequestPathUtils.parseAndCache(request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
MockHttpServletRequest request(String method, String uri, String servletPath) {
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest(method, uri);
|
||||||
|
request.setServletPath(servletPath);
|
||||||
|
ServletRequestPathUtils.parseAndCache(request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
MockServletContext servletContext(String... servletPath) {
|
||||||
|
MockServletContext servletContext = new MockServletContext();
|
||||||
|
ServletRegistration.Dynamic registration = servletContext.addServlet("servlet", Servlet.class);
|
||||||
|
for (String s : servletPath) {
|
||||||
|
registration.addMapping(s + "/*");
|
||||||
|
}
|
||||||
|
return servletContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user