Spring Boot 3.1.2: This method cannot decide ... error
You updated your Spring Boot and you get the following exception :(
This method cannot decide whether these patterns are Spring MVC patterns or not.
- The error appears with Spring Boot 3.1.2 (Spring Security 6.1.2)
- Example of solution: the case of H2 Database
- Get the introspector without injecting it in filterChain
The error appears with Spring Boot 3.1.2 (Spring Security 6.1.2)
Spring Boot 3.1.2 updates its dependencies to use the version 6.1.2 of Spring Security.
In this page, you can find the detail of the new requirements and the changes to make in your configuration to be 'compliant'.
The 'problem' has been introduced with this enhancement to Improve RequestMatcher Validation.
Spring Security 5.8.5 contains the same improvement and it is affected too.
To understand why this change is justified we can read the reason why a change in the request matchers is required:
CVE-2023-34035: Authorization rules can be misconfigured when using multiple servlets
The official announcement can be found here: Spring Security 5.6.12, 5.7.10, 5.8.5, 6.0.5, and 6.1.2 are available now.
This version of Spring Security introduces some new 'checks' to avoid possible security holes.
Some developers that like to work with the latest version of Spring (or that are testing it in Dev) they had the surprise that their build was not working anymore and they got the following error:
Caused by: java.lang.IllegalArgumentException: 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).
at org.springframework.util.Assert.isTrue(Assert.java:122) ~[spring-core-6.0.11.jar:6.0.11]
If you see this kind of error, probably your application is trying to start with more than 1 Servlet and Spring Boot doesn't like how you configured your Security.
To simplify
- Use mvcMatchers for endpoints handled by Spring (you need to use MvcRequestMatcher.Builder with HandlerMappingIntrospector)
- Use antMatchers for endpoints NOT handled by Spring (e.g. H2,
.antMatchers("/console").hasRole("ADMIN")
)
If your application is using only the default servlet of Spring (DispatcherServlet), you won't have this issue. Spring assumes that your matchers are mvcMatchers.
If you are using directly or indirectly more Servlet (see H2 example) you have to specify in the Spring Security configuration with paths are managed by Spring (mvcMatchers) or are not handled by Spring (antMatchers).
For a refresher about the recent changes in the matchers (from Spring 5.8) and many examples you can read: https://docs.spring.io/spring-security/reference/5.8/migration/servlet/config.html
You can find an example of solution with the case of H2 in the next chapter.
This is the explanation of a Spring Security developer:
The issue stems from the fact that MvcRequestMatcher is primarily designed to work for endpoints that will be serviced by Spring MVC.
This extends back to older CVEs and the addition of mvcMatchers. As you said, this left applications wondering whether they should pick antMatchers or mvcMatchers, and unfortunately antMatchers was often incorrectly selected.
The practical truth is that mvcMatchers should almost always be picked in a Spring MVC application (version 5 or earlier) since it is fit for purpose for requests that will get serviced by Spring MVC.
Doing otherwise may leave that application open to the earlier CVE that inspired mvcMatchers.
Update: The Spring classes have been updated in the release of Spring Security 6.1.3 and 6.2.x.
Here you can find the changes.
Example of solution: the case of H2 Database
If in your environment you are using H2 database and its console, probably you will encounter this issue.
I use H2 in memory for the test environment and the Exception came with the update to Spring Boot 3.1.2.
The problems originated from the fact that H2 uses its own Servlet and this 'breaks' the assumption done by Spring that all the paths are handled by Spring and a distinction between antMatcher and mvcMatcher has to be done in the configuration.
The easiest solution: no more console
The easy and lazy solution is to deactivate the H2 console, this avoids having an endpoint open in the application that is not managed by Spring:
spring.h2.console.enabled=false # setting it to false the H2 Servlet is not loaded anymore
spring.h2.console.path=/console # this is the endpoint opened that could generate the exception
The serious solution: differentiate between antMatchers and mvcMatchers
If you prefer this solution, which could be applied to other libraries than H2, in your Spring Security config you have to create an antMatcher for the external endpoints:
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
http
.[standard config]
.authorizeHttpRequests(auth -> auth.requestMatchers(antMatcher("/h2-console/**")).authenticated()) // or .permitAll() or .hasAuthority(...)
...
return http.build();
}
}
This code tells Spring that the console path is not a Spring Servlet, Spring knows that the antMatchers are for servlets managed by others.
The annoying part is now to do the same for the Spring managed path, e.g.:
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
public class YourClass ... {
MvcRequestMatcher.Builder mvcMatcherBuilder;
public YourClass(HandlerMappingIntrospector introspector) {
this.mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
}
...
.authorizeHttpRequests(auth -> auth
.requestMatchers(mvcMatcherBuilder.pattern(HttpMethod.POST, "/**")).hasAuthority(...)
}
For this solution, you need to import and use the HandlerMappingIntrospector
.
The next release of Spring Security may propose a more elegant solution to this issue, you can follow the discussion here: https://github.com/spring-projects/spring-security/issues/13568
Get the introspector without injecting it in filterChain
As we saw in the previous example the introspector is injected automatically in the filterChain parameters, here you have another example from the Spring Security official documentation.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector).servletPath("/path");
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvcMatcherBuilder.pattern("/admin")).hasRole("ADMIN")
.requestMatchers(mvcMatcherBuilder.pattern("/user")).hasRole("USER")
);
return http.build();
}
If you want to get the introspector directly without injection you can call it using it bean, e.g.:
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.context.ApplicationContext;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
public class IntrospectorUtil {
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
private IntrospectorUtil() {
}
public static HandlerMappingIntrospector getIntrospector(HttpSecurity http) {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
return context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class);
}
}
This is, more or less, what Spring is doing behind the scenes. Example of usage:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(IntrospectorUtil.getIntrospector(http)).servletPath("/path");
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvcMatcherBuilder.pattern("/admin")).hasRole("ADMIN")
.requestMatchers(mvcMatcherBuilder.pattern("/user")).hasRole("USER")
);
return http.build();
}