Add PathPatterRequestMatcher

Closes gh-16429
Clsoes gh-16430
This commit is contained in:
Josh Cummings 2025-02-21 13:24:55 -07:00
parent 4f25f0b90f
commit 588220a020
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
6 changed files with 596 additions and 44 deletions

View File

@ -264,11 +264,13 @@ public abstract class AbstractRequestMatcherRegistry<C> {
}
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.Builder#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());

View File

@ -577,15 +577,11 @@ http {
======
[[match-by-mvc]]
=== Using an MvcRequestMatcher
=== Matching by Servlet Path
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.
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:
However, if you have authorization rules from multiple servlets, you need to specify those:
.Match by MvcRequestMatcher
[tabs]
@ -594,16 +590,15 @@ Java::
+
[source,java,role="primary"]
----
@Bean
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
}
import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.withDefaults;
@Bean
SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) {
SecurityFilterChain appEndpoints(HttpSecurity http) {
PathPatternRequestMatcher.Builder mvc = withDefaults().servletPath("/spring-mvc");
http
.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()
);
@ -616,17 +611,15 @@ Kotlin::
[source,kotlin,role="secondary"]
----
@Bean
fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
@Bean
fun appEndpoints(http: HttpSecurity, mvc: MvcRequestMatcher.Builder): SecurityFilterChain =
fun appEndpoints(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(mvc.pattern("/my/controller/**"), hasAuthority("controller"))
authorize("/spring-mvc", "/admin/**", hasAuthority("admin"))
authorize("/spring-mvc", "/my/controller/**", hasAuthority("controller"))
authorize(anyRequest, authenticated)
}
}
}
----
Xml::
@ -634,16 +627,19 @@ Xml::
[source,xml,role="secondary"]
----
<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 pattern="/**" access="authenticated"/>
</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
* If you register more than one Spring MVC `DispatcherServlet` (thus requiring that one of them not be the default path)
[TIP]
=====
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]]
=== Using a Custom Matcher

View File

@ -46,6 +46,7 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.DelegatingFilterProxy;
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
@ -162,6 +163,8 @@ public class FilterChainProxy extends GenericFilterBean {
private FilterChainDecorator filterChainDecorator = new VirtualFilterChainDecorator();
private Filter springWebFilter = new ServletRequestPathFilter();
public FilterChainProxy() {
}
@ -210,27 +213,29 @@ public class FilterChainProxy extends GenericFilterBean {
throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.isEmpty()) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
this.springWebFilter.doFilter(firewallRequest, firewallResponse, (r, s) -> {
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.isEmpty()) {
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()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
// Deactivate path stripping as we exit the security filter chain
firewallRequest.reset();
chain.doFilter(req, res);
};
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
FilterChain reset = (req, res) -> {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
}
// 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));
}
}
}

View File

@ -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 + "]";
}
}
}

View File

@ -48,6 +48,7 @@ import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.servlet.TestMockHttpServletMappings;
import org.springframework.security.web.util.matcher.RequestMatcher;
import static org.assertj.core.api.Assertions.assertThat;
@ -166,6 +167,7 @@ public class FilterChainProxyTests {
FirewalledRequest fwr = mock(FirewalledRequest.class);
given(fwr.getRequestURI()).willReturn("/");
given(fwr.getContextPath()).willReturn("");
given(fwr.getHttpServletMapping()).willReturn(TestMockHttpServletMappings.defaultMapping());
this.fcp.setFirewall(fw);
given(fw.getFirewalledRequest(this.request)).willReturn(fwr);
given(this.matcher.matches(any(HttpServletRequest.class))).willReturn(false);
@ -183,9 +185,11 @@ public class FilterChainProxyTests {
FirewalledRequest firstFwr = mock(FirewalledRequest.class, "firstFwr");
given(firstFwr.getRequestURI()).willReturn("/");
given(firstFwr.getContextPath()).willReturn("");
given(firstFwr.getHttpServletMapping()).willReturn(TestMockHttpServletMappings.defaultMapping());
FirewalledRequest fwr = mock(FirewalledRequest.class, "fwr");
given(fwr.getRequestURI()).willReturn("/");
given(fwr.getContextPath()).willReturn("");
given(fwr.getHttpServletMapping()).willReturn(TestMockHttpServletMappings.defaultMapping());
given(fw.getFirewalledRequest(this.request)).willReturn(firstFwr);
given(fw.getFirewalledRequest(firstFwr)).willReturn(fwr);
given(fwr.getRequest()).willReturn(firstFwr);

View File

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