Skip to content

Commit

Permalink
Add PathPatterRequestMatcher
Browse files Browse the repository at this point in the history
Closes gh-16429
Clsoes gh-16430
  • Loading branch information
jzheaux committed Feb 21, 2025
1 parent 4f25f0b commit 588220a
Show file tree
Hide file tree
Showing 6 changed files with 596 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,13 @@ private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
}

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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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()
);
Expand All @@ -616,34 +611,35 @@ 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::
+
[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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
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
Expand Down Expand Up @@ -162,6 +163,8 @@ public class FilterChainProxy extends GenericFilterBean {

private FilterChainDecorator filterChainDecorator = new VirtualFilterChainDecorator();

private Filter springWebFilter = new ServletRequestPathFilter();

public FilterChainProxy() {
}

Expand Down Expand Up @@ -210,27 +213,29 @@ private void doFilterInternal(ServletRequest request, ServletResponse response,
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);
});
}

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

}

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

}

}
Loading

0 comments on commit 588220a

Please sign in to comment.