Skip to content

Commit

Permalink
Updates
Browse files Browse the repository at this point in the history
- Refined documentation
- Applied method naming feedback
  • Loading branch information
jzheaux committed Feb 7, 2025
1 parent a22f87d commit 9b77542
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -179,19 +179,6 @@ public C requestMatchers(RequestMatcher... requestMatchers) {
return chainRequestMatchers(Arrays.asList(requestMatchers));
}

/**
* Register the {@link RequestMatcher} represented by this builder
* @param builder the
* {@link org.springframework.security.web.util.matcher.RequestMatchers.Builder} to
* use
* @return the object that is chained after creating the {@link RequestMatcher}
* @since 6.5
*/
public C requestMatchers(org.springframework.security.web.util.matcher.RequestMatchers.Builder builder) {
Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest");
return chainRequestMatchers(List.of(builder.matcher()));
}

/**
* <p>
* If the {@link HandlerMappingIntrospector} is available in the classpath, maps to an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1343,11 +1343,11 @@ static class MvcRequestMatcherBuilderConfig {

@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
RequestMatchers.Builder mvc = RequestMatchers.servlet("/mvc");
RequestMatchers.Builder mvc = RequestMatchers.servletPath("/mvc");
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvc.uris("/path/**")).hasRole("USER")
.requestMatchers(mvc.pathPatterns("/path/**").matcher()).hasRole("USER")
)
.httpBasic(withDefaults());
// @formatter:on
Expand Down
41 changes: 36 additions & 5 deletions docs/modules/ROOT/pages/migration-7/web.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,42 @@ Xml::
----
======

== Use Absolute Authorization URIs
== Include the Servlet Path Prefix in Authorization Rules

The Java DSL now requires that all URIs be absolute (less any context root).
As of Spring Security 7, `AntPathRequestMatcher` and `MvcRequestMatcher` are no longer supported and the Java DSL requires that all URIs be absolute (less any context root).

This means any endpoints that are not part of the default servlet, xref:servlet/authorization/authorize-http-requests.adoc#match-by-mvc[the servlet path needs to be specified].
For URIs that match an extension, like `.jsp`, use `regexMatcher("\\.jsp$")`.
For many applications this will make no difference since most commonly all URIs listed are matched by the default servlet.

Alternatively, you can change each of your `String` URI authorization rules to xref:servlet/authorization/authorize-http-requests.adoc#security-matchers[use a `RequestMatcher`].
However, if you have other servlets with servlet path prefixes, xref:servlet/authorization/authorize-http-requests.adoc[then these paths need to be supplied separately].

For example, if I have a Spring MVC controller with `@RequestMapping("/orders")` and my MVC application is deployed to `/mvc` (instead of the default servlet), then the URI for this endpoint is `/mvc/orders`.
Historically, the Java DSL hasn't had a simple way to specify the servlet path prefix and Spring Security attempted to infer it.

Over time, we learned that these inference would surprise developers.
Instead of taking this responsibility away from developers, now it is simpler to specify the servlet path prefix like so:

[method,java]
----
RequestMatchers.Builder servlet = RequestMatchers.servlet("/mvc");
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(servlet.uris("/orders/**").matcher()).authenticated()
)
----


For paths that belong to the default servlet, use `RequestMatchers.request()` instead:

[method,java]
----
RequestMatchers.Builder request = RequestMatchers.request();
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(request.uris("/js/**").matcher()).authenticated()
)
----

Note that this doesn't address every kind of servlet since not all servlets have a path prefix.
For example, expressions that match the JSP Servlet might use an ant pattern `/**/*.jsp`.

There is not yet a general-purpose replacement for these, and so you are encouraged to use `RegexRequestMatcher`, like so: `regexMatcher("\\.jsp$")`.
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ SecurityFilterChain appEndpoints(HttpSecurity http) {

[TIP]
=====
There are several other components that create request matchers for you like `PathRequest#toStaticResources#atCommonLocations`
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]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@
* is, it should exclude any context or servlet path).
*
* <p>
* To also match the servlet, please see {@link RequestMatchers#servlet}
* To also match the servlet, please see {@link RequestMatchers#servletPath}
*
* <p>
* Note that the {@link org.springframework.web.servlet.HandlerMapping} that contains the
* related URI patterns must be using the same
* {@link org.springframework.web.util.pattern.PathPatternParser} configured in this
* class.
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
Expand All @@ -33,6 +34,7 @@

import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
Expand Down Expand Up @@ -96,16 +98,23 @@ public static Builder request() {

/**
* Create {@link RequestMatcher}s whose URIs are relative to the given
* {@code servletPath}.
* {@code servletPath} prefix.
*
* <p>
* The {@code servletPath} must correlate to a configured servlet in your application.
* The path must be of the format {@code /path}.
* The {@code servletPath} must correlate to a value that would match the result of
* {@link HttpServletRequest#getServletPath()} and its corresponding servlet.
*
* <p>
* That is, if you have a servlet mapping of {@code /path/*}, then
* {@link HttpServletRequest#getServletPath()} would return {@code /path} and so
* {@code /path} is what is specified here.
*
* Specify the path here without the trailing {@code /*}.
* @return a {@link Builder} that treats URIs as relative to the given
* {@code servletPath}
* @since 6.5
*/
public static Builder servlet(String servletPath) {
public static Builder servletPath(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");
Expand Down Expand Up @@ -147,25 +156,22 @@ public static final class Builder {

private final RequestMatcher dispatcherTypes;

private final RequestMatcher matchers;

private Builder() {
this(AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE,
AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE);
AnyRequestMatcher.INSTANCE);
}

private Builder(String servletPath) {
this(new ServletPathRequestMatcher(servletPath), AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE,
AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE);
AnyRequestMatcher.INSTANCE);
}

private Builder(RequestMatcher servletPath, RequestMatcher methods, RequestMatcher uris,
RequestMatcher dispatcherTypes, RequestMatcher matchers) {
RequestMatcher dispatcherTypes) {
this.servletPath = servletPath;
this.methods = methods;
this.uris = uris;
this.dispatcherTypes = dispatcherTypes;
this.matchers = matchers;
}

/**
Expand All @@ -178,48 +184,63 @@ public Builder methods(HttpMethod... methods) {
for (int i = 0; i < methods.length; i++) {
matchers[i] = new HttpMethodRequestMatcher(methods[i]);
}
return new Builder(this.servletPath, anyOf(matchers), this.uris, this.dispatcherTypes, this.matchers);
return new Builder(this.servletPath, anyOf(matchers), this.uris, this.dispatcherTypes);
}

/**
* Match requests with any of these URIs
* Match requests with any of these path patterns
*
* <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>
* URIs can be Ant patterns like {@code /path/**}.
* @param uris the URIs to match
* A more comprehensive list can be found at {@link PathPattern}.
* @param pathPatterns the path patterns to match
* @return the {@link Builder} for more configuration
*/
public Builder uris(String... uris) {
RequestMatcher[] matchers = new RequestMatcher[uris.length];
for (int i = 0; i < uris.length; i++) {
Assert.isTrue(uris[i].startsWith("/"), "pattern must start with '/'");
public Builder pathPatterns(String... pathPatterns) {
RequestMatcher[] matchers = new RequestMatcher[pathPatterns.length];
for (int i = 0; i < pathPatterns.length; i++) {
Assert.isTrue(pathPatterns[i].startsWith("/"), "path patterns must start with /");
PathPatternParser parser = PathPatternParser.defaultInstance;
matchers[i] = new PathPatternRequestMatcher(parser.parse(uris[i]));
matchers[i] = new PathPatternRequestMatcher(parser.parse(pathPatterns[i]));
}
return new Builder(this.servletPath, this.methods, anyOf(matchers), this.dispatcherTypes, this.matchers);
return new Builder(this.servletPath, this.methods, anyOf(matchers), this.dispatcherTypes);
}

/**
* Match requests with any of these {@link PathPattern}s
*
* <p>
* Use this when you have a non-default {@link PathPatternParser}
* @param uris the URIs to match
* @param pathPatterns the URIs to match
* @return the {@link Builder} for more configuration
*/
public Builder uris(PathPattern... uris) {
RequestMatcher[] matchers = new RequestMatcher[uris.length];
for (int i = 0; i < uris.length; i++) {
matchers[i] = new PathPatternRequestMatcher(uris[i]);
public Builder pathPatterns(PathPattern... pathPatterns) {
RequestMatcher[] matchers = new RequestMatcher[pathPatterns.length];
for (int i = 0; i < pathPatterns.length; i++) {
matchers[i] = new PathPatternRequestMatcher(pathPatterns[i]);
}
return new Builder(this.servletPath, this.methods, anyOf(matchers), this.dispatcherTypes, this.matchers);
return new Builder(this.servletPath, this.methods, anyOf(matchers), this.dispatcherTypes);
}

/**
* Match requests with any of these dispatcherTypes
*
* <p>
* URIs can be Ant patterns like {@code /path/**}.
* @param dispatcherTypes the {@link DispatcherType}s to match
* @return the {@link Builder} for more configuration
*/
Expand All @@ -228,24 +249,15 @@ public Builder dispatcherTypes(DispatcherType... dispatcherTypes) {
for (int i = 0; i < dispatcherTypes.length; i++) {
matchers[i] = new DispatcherTypeRequestMatcher(dispatcherTypes[i]);
}
return new Builder(this.servletPath, this.methods, this.uris, anyOf(matchers), this.matchers);
}

/**
* Match requests with any of these {@link RequestMatcher}s
* @param requestMatchers the {@link RequestMatchers}s to match
* @return the {@link Builder} for more configuration
*/
public Builder matching(RequestMatcher... requestMatchers) {
return new Builder(this.servletPath, this.methods, this.uris, this.dispatcherTypes, anyOf(requestMatchers));
return new Builder(this.servletPath, this.methods, this.uris, anyOf(matchers));
}

/**
* Create the {@link RequestMatcher}
* @return the composite {@link RequestMatcher}
*/
public RequestMatcher matcher() {
return allOf(this.servletPath, this.methods, this.uris, this.dispatcherTypes, this.matchers);
return allOf(this.servletPath, this.methods, this.uris, this.dispatcherTypes);
}

}
Expand All @@ -264,7 +276,15 @@ public String toString() {

}

private record ServletPathRequestMatcher(String path) implements RequestMatcher {
private static final class ServletPathRequestMatcher implements RequestMatcher {

private final String path;

private final AtomicReference<Boolean> servletExists = new AtomicReference();

ServletPathRequestMatcher(String servletPath) {
this.path = servletPath;
}

@Override
public boolean matches(HttpServletRequest request) {
Expand All @@ -274,16 +294,22 @@ public boolean matches(HttpServletRequest request) {
}

private boolean servletExists(HttpServletRequest request) {
if (request.getAttribute("org.springframework.test.web.servlet.MockMvc.MVC_RESULT_ATTRIBUTE") != null) {
return true;
}
ServletContext servletContext = request.getServletContext();
for (ServletRegistration registration : servletContext.getServletRegistrations().values()) {
if (registration.getMappings().contains(this.path + "/*")) {
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;
}
}
return false;
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) {
Expand Down Expand Up @@ -313,6 +339,7 @@ private static String getServletPathPrefix(HttpServletRequest request) {
public String toString() {
return "ServletPath [" + this.path + "]";
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,19 @@ void matcherWhenUriContainsServletPathThenNoMatch() {

@Test
void matcherWhenSameMethodThenMatchResult() {
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).uris("/uri").matcher();
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).pathPatterns("/uri").matcher();
assertThat(matcher.matches(request("/uri"))).isTrue();
}

@Test
void matcherWhenDifferentPathThenNoMatch() {
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).uris("/uri").matcher();
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).pathPatterns("/uri").matcher();
assertThat(matcher.matches(request("GET", "/urj", ""))).isFalse();
}

@Test
void matcherWhenDifferentMethodThenNoMatch() {
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).uris("/uri").matcher();
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).pathPatterns("/uri").matcher();
assertThat(matcher.matches(request("POST", "/mvc/uri", "/mvc"))).isFalse();
}

Expand Down
Loading

0 comments on commit 9b77542

Please sign in to comment.