Skip to content

Commit a22f87d

Browse files
committed
Add RequestMatchers.Builder
This static factory simplifes the creation of RequestMatchers that specify the servlet path and other request elements Closes gh-16430
1 parent 2dae803 commit a22f87d

File tree

6 files changed

+409
-27
lines changed

6 files changed

+409
-27
lines changed

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

+21-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -179,6 +179,19 @@ 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+
182195
/**
183196
* <p>
184197
* If the {@link HandlerMappingIntrospector} is available in the classpath, maps to an
@@ -264,11 +277,13 @@ private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
264277
}
265278

266279
private static String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
267-
String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. "
268-
+ "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); "
269-
+ "otherwise, please use requestMatchers(AntPathRequestMatcher).\n\n"
270-
+ "This is because there is more than one mappable servlet in your servlet context: %s.\n\n"
271-
+ "For each MvcRequestMatcher, call MvcRequestMatcher#setServletPath to indicate the servlet path.";
280+
String template = """
281+
This method cannot decide whether these patterns are Spring MVC patterns or not. \
282+
This is because there is more than one mappable servlet in your servlet context: %s.
283+
284+
To address this, please create one RequestMatchers#servlet for each servlet that has \
285+
authorized endpoints and use them to construct request matchers manually.
286+
""";
272287
Map<String, Collection<String>> mappings = new LinkedHashMap<>();
273288
for (ServletRegistration registration : registrations) {
274289
mappings.put(registration.getClassName(), registration.getMappings());

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

+40
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
6565
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
6666
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
67+
import org.springframework.security.web.util.matcher.RequestMatchers;
6768
import org.springframework.test.web.servlet.MockMvc;
6869
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
6970
import org.springframework.test.web.servlet.request.RequestPostProcessor;
@@ -72,6 +73,7 @@
7273
import org.springframework.web.bind.annotation.PostMapping;
7374
import org.springframework.web.bind.annotation.RequestMapping;
7475
import org.springframework.web.bind.annotation.RestController;
76+
import org.springframework.web.servlet.DispatcherServlet;
7577
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
7678
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
7779

@@ -667,6 +669,19 @@ public void getWhenExcludeAuthorizationObservationsThenUnobserved() throws Excep
667669
verifyNoInteractions(handler);
668670
}
669671

672+
@Test
673+
public void requestMatchersWhenMultipleDispatcherServletsAndPathBeanThenAllows() throws Exception {
674+
this.spring.register(MvcRequestMatcherBuilderConfig.class, BasicController.class)
675+
.postProcessor((context) -> context.getServletContext()
676+
.addServlet("otherDispatcherServlet", DispatcherServlet.class)
677+
.addMapping("/mvc"))
678+
.autowire();
679+
this.mvc.perform(get("/mvc/path").servletPath("/mvc").with(user("user"))).andExpect(status().isOk());
680+
this.mvc.perform(get("/mvc/path").servletPath("/mvc").with(user("user").roles("DENIED")))
681+
.andExpect(status().isForbidden());
682+
this.mvc.perform(get("/path").with(user("user"))).andExpect(status().isForbidden());
683+
}
684+
670685
@Configuration
671686
@EnableWebSecurity
672687
static class GrantedAuthorityDefaultHasRoleConfig {
@@ -1262,6 +1277,10 @@ void rootGet() {
12621277
void rootPost() {
12631278
}
12641279

1280+
@GetMapping("/path")
1281+
void path() {
1282+
}
1283+
12651284
}
12661285

12671286
@Configuration
@@ -1317,4 +1336,25 @@ SecurityObservationSettings observabilityDefaults() {
13171336

13181337
}
13191338

1339+
@Configuration
1340+
@EnableWebSecurity
1341+
@EnableWebMvc
1342+
static class MvcRequestMatcherBuilderConfig {
1343+
1344+
@Bean
1345+
SecurityFilterChain security(HttpSecurity http) throws Exception {
1346+
RequestMatchers.Builder mvc = RequestMatchers.servlet("/mvc");
1347+
// @formatter:off
1348+
http
1349+
.authorizeHttpRequests((authorize) -> authorize
1350+
.requestMatchers(mvc.uris("/path/**")).hasRole("USER")
1351+
)
1352+
.httpBasic(withDefaults());
1353+
// @formatter:on
1354+
1355+
return http.build();
1356+
}
1357+
1358+
}
1359+
13201360
}

Diff for: docs/modules/ROOT/pages/migration-7/web.adoc

+9
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,12 @@ Xml::
102102
</b:bean>
103103
----
104104
======
105+
106+
== Use Absolute Authorization URIs
107+
108+
The Java DSL now requires that all URIs be absolute (less any context root).
109+
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$")`.
112+
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`].

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

+36-21
Original file line numberDiff line numberDiff line change
@@ -577,15 +577,11 @@ http {
577577
======
578578

579579
[[match-by-mvc]]
580-
=== Using an MvcRequestMatcher
580+
=== Matching by Servlet Path
581581

582582
Generally speaking, you can use `requestMatchers(String)` as demonstrated above.
583583

584-
However, if you map Spring MVC to a different servlet path, then you need to account for that in your security configuration.
585-
586-
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.
587-
588-
You need to use `MvcRequestMatcher` to split the servlet path and the controller path in your configuration like so:
584+
However, if you have authorization rules from multiple servlets, you need to specify those:
589585

590586
.Match by MvcRequestMatcher
591587
[tabs]
@@ -594,16 +590,14 @@ Java::
594590
+
595591
[source,java,role="primary"]
596592
----
597-
@Bean
598-
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
599-
return new MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
600-
}
593+
import static org.springframework.security.web.servlet.util.matcher.RequestMatchers.servlet;
601594
602595
@Bean
603-
SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) {
596+
SecurityFilterChain appEndpoints(HttpSecurity http) {
604597
http
605598
.authorizeHttpRequests((authorize) -> authorize
606-
.requestMatchers(mvc.pattern("/my/controller/**")).hasAuthority("controller")
599+
.requestMatchers(servlet("/spring-mvc").uris("/admin/**")).hasAuthority("admin")
600+
.requestMatchers(servlet("/spring-mvc").uris("/my/controller/**")).hasAuthority("controller")
607601
.anyRequest().authenticated()
608602
);
609603
@@ -616,34 +610,55 @@ Kotlin::
616610
[source,kotlin,role="secondary"]
617611
----
618612
@Bean
619-
fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
620-
MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
621-
622-
@Bean
623-
fun appEndpoints(http: HttpSecurity, mvc: MvcRequestMatcher.Builder): SecurityFilterChain =
613+
fun appEndpoints(http: HttpSecurity): SecurityFilterChain {
624614
http {
625615
authorizeHttpRequests {
626-
authorize(mvc.pattern("/my/controller/**"), hasAuthority("controller"))
616+
authorize("/spring-mvc", "/admin/**", hasAuthority("admin"))
617+
authorize("/spring-mvc", "/my/controller/**", hasAuthority("controller"))
627618
authorize(anyRequest, authenticated)
628619
}
629620
}
621+
}
630622
----
631623
632624
Xml::
633625
+
634626
[source,xml,role="secondary"]
635627
----
636628
<http>
629+
<intercept-url servlet-path="/spring-mvc" pattern="/admin/**" access="hasAuthority('admin')"/>
637630
<intercept-url servlet-path="/spring-mvc" pattern="/my/controller/**" access="hasAuthority('controller')"/>
638631
<intercept-url pattern="/**" access="authenticated"/>
639632
</http>
640633
----
641634
======
642635

643-
This need can arise in at least two different ways:
636+
This is because Spring Security requires all URIs to be absolute (minus the context path).
637+
638+
With Java, note that the `ServletRequestMatcherBuilders` return value can be reused, reducing repeated boilerplate:
639+
640+
[source,java,role="primary"]
641+
----
642+
import static org.springframework.security.web.servlet.util.matcher.RequestMatchers.servlet;
643+
644+
@Bean
645+
SecurityFilterChain appEndpoints(HttpSecurity http) {
646+
RequestMatchers.Builder mvc = servlet("/spring-mvc");
647+
http
648+
.authorizeHttpRequests((authorize) -> authorize
649+
.requestMatchers(mvc.uris("/admin/**")).hasAuthority("admin")
650+
.requestMatchers(mvc.uris("/my/controller/**")).hasAuthority("controller")
651+
.anyRequest().authenticated()
652+
);
644653
645-
* If you use the `spring.mvc.servlet.path` Boot property to change the default path (`/`) to something else
646-
* If you register more than one Spring MVC `DispatcherServlet` (thus requiring that one of them not be the default path)
654+
return http.build();
655+
}
656+
----
657+
658+
[TIP]
659+
=====
660+
There are several other components that create request matchers for you like `PathRequest#toStaticResources#atCommonLocations`
661+
=====
647662

648663
[[match-by-custom]]
649664
=== Using a Custom Matcher

0 commit comments

Comments
 (0)