Skip to content

Commit 588220a

Browse files
committed
Add PathPatterRequestMatcher
Closes gh-16429 Clsoes gh-16430
1 parent 4f25f0b commit 588220a

File tree

6 files changed

+596
-44
lines changed

6 files changed

+596
-44
lines changed

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,13 @@ private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
264264
}
265265

266266
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.";
267+
String template = """
268+
This method cannot decide whether these patterns are Spring MVC patterns or not. \
269+
This is because there is more than one mappable servlet in your servlet context: %s.
270+
271+
To address this, please create one PathPatternRequestMatcher.Builder#servletPath for each servlet that has \
272+
authorized endpoints and use them to construct request matchers manually.
273+
""";
272274
Map<String, Collection<String>> mappings = new LinkedHashMap<>();
273275
for (ServletRegistration registration : registrations) {
274276
mappings.put(registration.getClassName(), registration.getMappings());

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

+17-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,15 @@ 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.PathPatternRequestMatcher.withDefaults;
601594
602595
@Bean
603-
SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) {
596+
SecurityFilterChain appEndpoints(HttpSecurity http) {
597+
PathPatternRequestMatcher.Builder mvc = withDefaults().servletPath("/spring-mvc");
604598
http
605599
.authorizeHttpRequests((authorize) -> authorize
606-
.requestMatchers(mvc.pattern("/my/controller/**")).hasAuthority("controller")
600+
.requestMatchers(mvc.matcher("/admin/**")).hasAuthority("admin")
601+
.requestMatchers(mvc.matcher("/my/controller/**")).hasAuthority("controller")
607602
.anyRequest().authenticated()
608603
);
609604
@@ -616,34 +611,35 @@ Kotlin::
616611
[source,kotlin,role="secondary"]
617612
----
618613
@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 =
614+
fun appEndpoints(http: HttpSecurity): SecurityFilterChain {
624615
http {
625616
authorizeHttpRequests {
626-
authorize(mvc.pattern("/my/controller/**"), hasAuthority("controller"))
617+
authorize("/spring-mvc", "/admin/**", hasAuthority("admin"))
618+
authorize("/spring-mvc", "/my/controller/**", hasAuthority("controller"))
627619
authorize(anyRequest, authenticated)
628620
}
629621
}
622+
}
630623
----
631624
632625
Xml::
633626
+
634627
[source,xml,role="secondary"]
635628
----
636629
<http>
630+
<intercept-url servlet-path="/spring-mvc" pattern="/admin/**" access="hasAuthority('admin')"/>
637631
<intercept-url servlet-path="/spring-mvc" pattern="/my/controller/**" access="hasAuthority('controller')"/>
638632
<intercept-url pattern="/**" access="authenticated"/>
639633
</http>
640634
----
641635
======
642636

643-
This need can arise in at least two different ways:
637+
This is because Spring Security requires all URIs to be absolute (minus the context path).
644638

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)
639+
[TIP]
640+
=====
641+
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`]
642+
=====
647643

648644
[[match-by-custom]]
649645
=== Using a Custom Matcher

web/src/main/java/org/springframework/security/web/FilterChainProxy.java

+42-18
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.util.Assert;
4747
import org.springframework.web.filter.DelegatingFilterProxy;
4848
import org.springframework.web.filter.GenericFilterBean;
49+
import org.springframework.web.filter.ServletRequestPathFilter;
4950

5051
/**
5152
* Delegates {@code Filter} requests to a list of Spring-managed filter beans. As of
@@ -162,6 +163,8 @@ public class FilterChainProxy extends GenericFilterBean {
162163

163164
private FilterChainDecorator filterChainDecorator = new VirtualFilterChainDecorator();
164165

166+
private Filter springWebFilter = new ServletRequestPathFilter();
167+
165168
public FilterChainProxy() {
166169
}
167170

@@ -210,27 +213,29 @@ private void doFilterInternal(ServletRequest request, ServletResponse response,
210213
throws IOException, ServletException {
211214
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
212215
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
213-
List<Filter> filters = getFilters(firewallRequest);
214-
if (filters == null || filters.isEmpty()) {
215-
if (logger.isTraceEnabled()) {
216-
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
216+
this.springWebFilter.doFilter(firewallRequest, firewallResponse, (r, s) -> {
217+
List<Filter> filters = getFilters(firewallRequest);
218+
if (filters == null || filters.isEmpty()) {
219+
if (logger.isTraceEnabled()) {
220+
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
221+
}
222+
firewallRequest.reset();
223+
this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
224+
return;
217225
}
218-
firewallRequest.reset();
219-
this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
220-
return;
221-
}
222-
if (logger.isDebugEnabled()) {
223-
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
224-
}
225-
FilterChain reset = (req, res) -> {
226226
if (logger.isDebugEnabled()) {
227-
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
227+
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
228228
}
229-
// Deactivate path stripping as we exit the security filter chain
230-
firewallRequest.reset();
231-
chain.doFilter(req, res);
232-
};
233-
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
229+
FilterChain reset = (req, res) -> {
230+
if (logger.isDebugEnabled()) {
231+
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
232+
}
233+
// Deactivate path stripping as we exit the security filter chain
234+
firewallRequest.reset();
235+
chain.doFilter(req, res);
236+
};
237+
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
238+
});
234239
}
235240

236241
/**
@@ -447,4 +452,23 @@ public FilterChain decorate(FilterChain original, List<Filter> filters) {
447452

448453
}
449454

455+
private static final class FirewallFilter implements Filter {
456+
457+
private final HttpFirewall firewall;
458+
459+
private FirewallFilter(HttpFirewall firewall) {
460+
this.firewall = firewall;
461+
}
462+
463+
@Override
464+
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
465+
throws IOException, ServletException {
466+
HttpServletRequest request = (HttpServletRequest) servletRequest;
467+
HttpServletResponse response = (HttpServletResponse) servletResponse;
468+
filterChain.doFilter(this.firewall.getFirewalledRequest(request),
469+
this.firewall.getFirewalledResponse(response));
470+
}
471+
472+
}
473+
450474
}

0 commit comments

Comments
 (0)