Skip to content

Commit 9b77542

Browse files
committed
Updates
- Refined documentation - Applied method naming feedback
1 parent a22f87d commit 9b77542

File tree

8 files changed

+133
-85
lines changed

8 files changed

+133
-85
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -179,19 +179,6 @@ public C requestMatchers(RequestMatcher... requestMatchers) {
179179
return chainRequestMatchers(Arrays.asList(requestMatchers));
180180
}
181181

182-
/**
183-
* Register the {@link RequestMatcher} represented by this builder
184-
* @param builder the
185-
* {@link org.springframework.security.web.util.matcher.RequestMatchers.Builder} to
186-
* use
187-
* @return the object that is chained after creating the {@link RequestMatcher}
188-
* @since 6.5
189-
*/
190-
public C requestMatchers(org.springframework.security.web.util.matcher.RequestMatchers.Builder builder) {
191-
Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest");
192-
return chainRequestMatchers(List.of(builder.matcher()));
193-
}
194-
195182
/**
196183
* <p>
197184
* If the {@link HandlerMappingIntrospector} is available in the classpath, maps to an

config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,11 +1343,11 @@ static class MvcRequestMatcherBuilderConfig {
13431343

13441344
@Bean
13451345
SecurityFilterChain security(HttpSecurity http) throws Exception {
1346-
RequestMatchers.Builder mvc = RequestMatchers.servlet("/mvc");
1346+
RequestMatchers.Builder mvc = RequestMatchers.servletPath("/mvc");
13471347
// @formatter:off
13481348
http
13491349
.authorizeHttpRequests((authorize) -> authorize
1350-
.requestMatchers(mvc.uris("/path/**")).hasRole("USER")
1350+
.requestMatchers(mvc.pathPatterns("/path/**").matcher()).hasRole("USER")
13511351
)
13521352
.httpBasic(withDefaults());
13531353
// @formatter:on

docs/modules/ROOT/pages/migration-7/web.adoc

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,42 @@ Xml::
103103
----
104104
======
105105

106-
== Use Absolute Authorization URIs
106+
== Include the Servlet Path Prefix in Authorization Rules
107107

108-
The Java DSL now requires that all URIs be absolute (less any context root).
108+
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).
109109

110-
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].
111-
For URIs that match an extension, like `.jsp`, use `regexMatcher("\\.jsp$")`.
110+
For many applications this will make no difference since most commonly all URIs listed are matched by the default servlet.
112111

113-
Alternatively, you can change each of your `String` URI authorization rules to xref:servlet/authorization/authorize-http-requests.adoc#security-matchers[use a `RequestMatcher`].
112+
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].
113+
114+
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`.
115+
Historically, the Java DSL hasn't had a simple way to specify the servlet path prefix and Spring Security attempted to infer it.
116+
117+
Over time, we learned that these inference would surprise developers.
118+
Instead of taking this responsibility away from developers, now it is simpler to specify the servlet path prefix like so:
119+
120+
[method,java]
121+
----
122+
RequestMatchers.Builder servlet = RequestMatchers.servlet("/mvc");
123+
http
124+
.authorizeHttpRequests((authorize) -> authorize
125+
.requestMatchers(servlet.uris("/orders/**").matcher()).authenticated()
126+
)
127+
----
128+
129+
130+
For paths that belong to the default servlet, use `RequestMatchers.request()` instead:
131+
132+
[method,java]
133+
----
134+
RequestMatchers.Builder request = RequestMatchers.request();
135+
http
136+
.authorizeHttpRequests((authorize) -> authorize
137+
.requestMatchers(request.uris("/js/**").matcher()).authenticated()
138+
)
139+
----
140+
141+
Note that this doesn't address every kind of servlet since not all servlets have a path prefix.
142+
For example, expressions that match the JSP Servlet might use an ant pattern `/**/*.jsp`.
143+
144+
There is not yet a general-purpose replacement for these, and so you are encouraged to use `RegexRequestMatcher`, like so: `regexMatcher("\\.jsp$")`.

docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@ SecurityFilterChain appEndpoints(HttpSecurity http) {
657657

658658
[TIP]
659659
=====
660-
There are several other components that create request matchers for you like `PathRequest#toStaticResources#atCommonLocations`
660+
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`]
661661
=====
662662

663663
[[match-by-custom]]

web/src/main/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcher.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@
3434
* is, it should exclude any context or servlet path).
3535
*
3636
* <p>
37-
* To also match the servlet, please see {@link RequestMatchers#servlet}
37+
* To also match the servlet, please see {@link RequestMatchers#servletPath}
3838
*
3939
* <p>
4040
* Note that the {@link org.springframework.web.servlet.HandlerMapping} that contains the
41-
* related URI patterns must be using the same
42-
* {@link org.springframework.web.util.pattern.PathPatternParser} configured in this
43-
* class.
41+
* related URI patterns must be using {@link PathPatternParser#defaultInstance}. If that
42+
* is not the case, use {@link PathPatternParser} to parse your path and provide a
43+
* {@link PathPattern} in the constructor.
4444
* </p>
4545
*
4646
* @author Josh Cummings

web/src/main/java/org/springframework/security/web/util/matcher/RequestMatchers.java

Lines changed: 76 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Objects;
25+
import java.util.concurrent.atomic.AtomicReference;
2526

2627
import jakarta.servlet.DispatcherType;
2728
import jakarta.servlet.RequestDispatcher;
@@ -33,6 +34,7 @@
3334

3435
import org.springframework.http.HttpMethod;
3536
import org.springframework.lang.Nullable;
37+
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
3638
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
3739
import org.springframework.util.Assert;
3840
import org.springframework.util.ObjectUtils;
@@ -96,16 +98,23 @@ public static Builder request() {
9698

9799
/**
98100
* Create {@link RequestMatcher}s whose URIs are relative to the given
99-
* {@code servletPath}.
101+
* {@code servletPath} prefix.
100102
*
101103
* <p>
102-
* The {@code servletPath} must correlate to a configured servlet in your application.
103-
* The path must be of the format {@code /path}.
104+
* The {@code servletPath} must correlate to a value that would match the result of
105+
* {@link HttpServletRequest#getServletPath()} and its corresponding servlet.
106+
*
107+
* <p>
108+
* That is, if you have a servlet mapping of {@code /path/*}, then
109+
* {@link HttpServletRequest#getServletPath()} would return {@code /path} and so
110+
* {@code /path} is what is specified here.
111+
*
112+
* Specify the path here without the trailing {@code /*}.
104113
* @return a {@link Builder} that treats URIs as relative to the given
105114
* {@code servletPath}
106115
* @since 6.5
107116
*/
108-
public static Builder servlet(String servletPath) {
117+
public static Builder servletPath(String servletPath) {
109118
Assert.notNull(servletPath, "servletPath cannot be null");
110119
Assert.isTrue(servletPath.startsWith("/"), "servletPath must start with '/'");
111120
Assert.isTrue(!servletPath.endsWith("/"), "servletPath must not end with a slash");
@@ -147,25 +156,22 @@ public static final class Builder {
147156

148157
private final RequestMatcher dispatcherTypes;
149158

150-
private final RequestMatcher matchers;
151-
152159
private Builder() {
153160
this(AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE,
154-
AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE);
161+
AnyRequestMatcher.INSTANCE);
155162
}
156163

157164
private Builder(String servletPath) {
158165
this(new ServletPathRequestMatcher(servletPath), AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE,
159-
AnyRequestMatcher.INSTANCE, AnyRequestMatcher.INSTANCE);
166+
AnyRequestMatcher.INSTANCE);
160167
}
161168

162169
private Builder(RequestMatcher servletPath, RequestMatcher methods, RequestMatcher uris,
163-
RequestMatcher dispatcherTypes, RequestMatcher matchers) {
170+
RequestMatcher dispatcherTypes) {
164171
this.servletPath = servletPath;
165172
this.methods = methods;
166173
this.uris = uris;
167174
this.dispatcherTypes = dispatcherTypes;
168-
this.matchers = matchers;
169175
}
170176

171177
/**
@@ -178,48 +184,63 @@ public Builder methods(HttpMethod... methods) {
178184
for (int i = 0; i < methods.length; i++) {
179185
matchers[i] = new HttpMethodRequestMatcher(methods[i]);
180186
}
181-
return new Builder(this.servletPath, anyOf(matchers), this.uris, this.dispatcherTypes, this.matchers);
187+
return new Builder(this.servletPath, anyOf(matchers), this.uris, this.dispatcherTypes);
182188
}
183189

184190
/**
185-
* Match requests with any of these URIs
191+
* Match requests with any of these path patterns
192+
*
193+
* <p>
194+
* Path patterns always start with a slash and may contain placeholders. They can
195+
* also be followed by {@code /**} to signify all URIs under a given path.
196+
*
197+
* <p>
198+
* These must be specified relative to any servlet path prefix (meaning you should
199+
* exclude the context path and any servlet path prefix in stating your pattern).
200+
*
201+
* <p>
202+
* The following are valid patterns and their meaning
203+
* <ul>
204+
* <li>{@code /path} - match exactly and only `/path`</li>
205+
* <li>{@code /path/**} - match `/path` and any of its descendents</li>
206+
* <li>{@code /path/{value}/**} - match `/path/subdirectory` and any of its
207+
* descendents, capturing the value of the subdirectory in
208+
* {@link RequestAuthorizationContext#getVariables()}</li>
209+
* </ul>
186210
*
187211
* <p>
188-
* URIs can be Ant patterns like {@code /path/**}.
189-
* @param uris the URIs to match
212+
* A more comprehensive list can be found at {@link PathPattern}.
213+
* @param pathPatterns the path patterns to match
190214
* @return the {@link Builder} for more configuration
191215
*/
192-
public Builder uris(String... uris) {
193-
RequestMatcher[] matchers = new RequestMatcher[uris.length];
194-
for (int i = 0; i < uris.length; i++) {
195-
Assert.isTrue(uris[i].startsWith("/"), "pattern must start with '/'");
216+
public Builder pathPatterns(String... pathPatterns) {
217+
RequestMatcher[] matchers = new RequestMatcher[pathPatterns.length];
218+
for (int i = 0; i < pathPatterns.length; i++) {
219+
Assert.isTrue(pathPatterns[i].startsWith("/"), "path patterns must start with /");
196220
PathPatternParser parser = PathPatternParser.defaultInstance;
197-
matchers[i] = new PathPatternRequestMatcher(parser.parse(uris[i]));
221+
matchers[i] = new PathPatternRequestMatcher(parser.parse(pathPatterns[i]));
198222
}
199-
return new Builder(this.servletPath, this.methods, anyOf(matchers), this.dispatcherTypes, this.matchers);
223+
return new Builder(this.servletPath, this.methods, anyOf(matchers), this.dispatcherTypes);
200224
}
201225

202226
/**
203227
* Match requests with any of these {@link PathPattern}s
204228
*
205229
* <p>
206230
* Use this when you have a non-default {@link PathPatternParser}
207-
* @param uris the URIs to match
231+
* @param pathPatterns the URIs to match
208232
* @return the {@link Builder} for more configuration
209233
*/
210-
public Builder uris(PathPattern... uris) {
211-
RequestMatcher[] matchers = new RequestMatcher[uris.length];
212-
for (int i = 0; i < uris.length; i++) {
213-
matchers[i] = new PathPatternRequestMatcher(uris[i]);
234+
public Builder pathPatterns(PathPattern... pathPatterns) {
235+
RequestMatcher[] matchers = new RequestMatcher[pathPatterns.length];
236+
for (int i = 0; i < pathPatterns.length; i++) {
237+
matchers[i] = new PathPatternRequestMatcher(pathPatterns[i]);
214238
}
215-
return new Builder(this.servletPath, this.methods, anyOf(matchers), this.dispatcherTypes, this.matchers);
239+
return new Builder(this.servletPath, this.methods, anyOf(matchers), this.dispatcherTypes);
216240
}
217241

218242
/**
219243
* Match requests with any of these dispatcherTypes
220-
*
221-
* <p>
222-
* URIs can be Ant patterns like {@code /path/**}.
223244
* @param dispatcherTypes the {@link DispatcherType}s to match
224245
* @return the {@link Builder} for more configuration
225246
*/
@@ -228,24 +249,15 @@ public Builder dispatcherTypes(DispatcherType... dispatcherTypes) {
228249
for (int i = 0; i < dispatcherTypes.length; i++) {
229250
matchers[i] = new DispatcherTypeRequestMatcher(dispatcherTypes[i]);
230251
}
231-
return new Builder(this.servletPath, this.methods, this.uris, anyOf(matchers), this.matchers);
232-
}
233-
234-
/**
235-
* Match requests with any of these {@link RequestMatcher}s
236-
* @param requestMatchers the {@link RequestMatchers}s to match
237-
* @return the {@link Builder} for more configuration
238-
*/
239-
public Builder matching(RequestMatcher... requestMatchers) {
240-
return new Builder(this.servletPath, this.methods, this.uris, this.dispatcherTypes, anyOf(requestMatchers));
252+
return new Builder(this.servletPath, this.methods, this.uris, anyOf(matchers));
241253
}
242254

243255
/**
244256
* Create the {@link RequestMatcher}
245257
* @return the composite {@link RequestMatcher}
246258
*/
247259
public RequestMatcher matcher() {
248-
return allOf(this.servletPath, this.methods, this.uris, this.dispatcherTypes, this.matchers);
260+
return allOf(this.servletPath, this.methods, this.uris, this.dispatcherTypes);
249261
}
250262

251263
}
@@ -264,7 +276,15 @@ public String toString() {
264276

265277
}
266278

267-
private record ServletPathRequestMatcher(String path) implements RequestMatcher {
279+
private static final class ServletPathRequestMatcher implements RequestMatcher {
280+
281+
private final String path;
282+
283+
private final AtomicReference<Boolean> servletExists = new AtomicReference();
284+
285+
ServletPathRequestMatcher(String servletPath) {
286+
this.path = servletPath;
287+
}
268288

269289
@Override
270290
public boolean matches(HttpServletRequest request) {
@@ -274,16 +294,22 @@ public boolean matches(HttpServletRequest request) {
274294
}
275295

276296
private boolean servletExists(HttpServletRequest request) {
277-
if (request.getAttribute("org.springframework.test.web.servlet.MockMvc.MVC_RESULT_ATTRIBUTE") != null) {
278-
return true;
279-
}
280-
ServletContext servletContext = request.getServletContext();
281-
for (ServletRegistration registration : servletContext.getServletRegistrations().values()) {
282-
if (registration.getMappings().contains(this.path + "/*")) {
297+
return this.servletExists.updateAndGet((value) -> {
298+
if (value != null) {
299+
return value;
300+
}
301+
if (request.getAttribute("org.springframework.test.web.servlet.MockMvc.MVC_RESULT_ATTRIBUTE") != null) {
283302
return true;
284303
}
285-
}
286-
return false;
304+
for (ServletRegistration registration : request.getServletContext()
305+
.getServletRegistrations()
306+
.values()) {
307+
if (registration.getMappings().contains(this.path + "/*")) {
308+
return true;
309+
}
310+
}
311+
return false;
312+
});
287313
}
288314

289315
private Map<String, Collection<String>> registrationMappings(HttpServletRequest request) {
@@ -313,6 +339,7 @@ private static String getServletPathPrefix(HttpServletRequest request) {
313339
public String toString() {
314340
return "ServletPath [" + this.path + "]";
315341
}
342+
316343
}
317344

318345
}

web/src/test/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcherTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,19 @@ void matcherWhenUriContainsServletPathThenNoMatch() {
5757

5858
@Test
5959
void matcherWhenSameMethodThenMatchResult() {
60-
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).uris("/uri").matcher();
60+
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).pathPatterns("/uri").matcher();
6161
assertThat(matcher.matches(request("/uri"))).isTrue();
6262
}
6363

6464
@Test
6565
void matcherWhenDifferentPathThenNoMatch() {
66-
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).uris("/uri").matcher();
66+
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).pathPatterns("/uri").matcher();
6767
assertThat(matcher.matches(request("GET", "/urj", ""))).isFalse();
6868
}
6969

7070
@Test
7171
void matcherWhenDifferentMethodThenNoMatch() {
72-
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).uris("/uri").matcher();
72+
RequestMatcher matcher = RequestMatchers.request().methods(HttpMethod.GET).pathPatterns("/uri").matcher();
7373
assertThat(matcher.matches(request("POST", "/mvc/uri", "/mvc"))).isFalse();
7474
}
7575

0 commit comments

Comments
 (0)