Disable suffix pattern matching in Spring MVC

This commit disables by default suffix pattern matching in Spring MVC
applications. As described in the Spring MVC documentation (see
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-requestmapping-suffix-pattern-match),
this is considered as best practice.

This change also introduces new configuration properties to achieve
similar results in a safer way (using query parameters) or to rollback
to the former default.

Closes gh-11105
pull/11638/head
Brian Clozel 7 years ago
parent 2bf662f231
commit 67e5897c40

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -101,6 +101,7 @@ import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceChainRegistration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@ -214,8 +215,23 @@ public class WebMvcAutoConfiguration {
}
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(this.mvcProperties
.getPathMatch().isUseSuffixPattern());
configurer.setUseRegisteredSuffixPatternMatch(this.mvcProperties
.getPathMatch().isUseRegisteredSuffixPattern());
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
WebMvcProperties.ContentNegotiation contentNegotiation
= this.mvcProperties.getContentNegotiation();
configurer.favorPathExtension(contentNegotiation.isFavorPathExtension());
configurer.favorParameter(contentNegotiation.isFavorParameter());
if (contentNegotiation.getParameterName() != null) {
configurer.parameterName(contentNegotiation.getParameterName());
}
Map<String, MediaType> mediaTypes = this.mvcProperties.getMediaTypes();
for (Entry<String, MediaType> mediaType : mediaTypes.entrySet()) {
configurer.mediaType(mediaType.getKey(), mediaType.getValue());
@ -308,8 +324,8 @@ public class WebMvcAutoConfiguration {
registry.addResourceHandler("/webjars/**")
.addResourceLocations(
"classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
@ -317,8 +333,8 @@ public class WebMvcAutoConfiguration {
registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(
this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
}
@ -450,8 +466,8 @@ public class WebMvcAutoConfiguration {
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
adapter.setIgnoreDefaultModelOnRedirect(this.mvcProperties == null ? true
: this.mvcProperties.isIgnoreDefaultModelOnRedirect());
adapter.setIgnoreDefaultModelOnRedirect(this.mvcProperties == null
|| this.mvcProperties.isIgnoreDefaultModelOnRedirect());
return adapter;
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,6 +32,7 @@ import org.springframework.validation.DefaultMessageCodesResolver;
* @author Sébastien Deleuze
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Brian Clozel
* @since 1.1
*/
@ConfigurationProperties(prefix = "spring.mvc")
@ -102,6 +103,10 @@ public class WebMvcProperties {
private final View view = new View();
private final ContentNegotiation contentNegotiation = new ContentNegotiation();
private final PathMatch pathMatch = new PathMatch();
public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
return this.messageCodesResolverFormat;
}
@ -204,6 +209,14 @@ public class WebMvcProperties {
return this.view;
}
public ContentNegotiation getContentNegotiation() {
return this.contentNegotiation;
}
public PathMatch getPathMatch() {
return this.pathMatch;
}
public static class Async {
/**
@ -270,6 +283,84 @@ public class WebMvcProperties {
}
public static class ContentNegotiation {
/**
* Whether the path extension in the URL path should be used to determine
* the requested media type. If enabled a request "/users.pdf" will be
* interpreted as a request for "application/pdf" regardless of the 'Accept' header.
*/
private boolean favorPathExtension = false;
/**
* Whether a request parameter ("format" by default) should be used to
* determine the requested media type.
*/
private boolean favorParameter = false;
/**
* Query parameter name to use when "favor-parameter" is enabled.
*/
private String parameterName;
public boolean isFavorPathExtension() {
return this.favorPathExtension;
}
public void setFavorPathExtension(boolean favorPathExtension) {
this.favorPathExtension = favorPathExtension;
}
public boolean isFavorParameter() {
return this.favorParameter;
}
public void setFavorParameter(boolean favorParameter) {
this.favorParameter = favorParameter;
}
public String getParameterName() {
return this.parameterName;
}
public void setParameterName(String parameterName) {
this.parameterName = parameterName;
}
}
public static class PathMatch {
/**
* Whether to use suffix pattern match (".*") when matching patterns to
* requests. If enabled a method mapped to "/users" also matches to "/users.*".
*/
private boolean useSuffixPattern = false;
/**
* Whether suffix pattern matching should work only against path extensions
* explicitly registered with "spring.mvc.media-types.*".
* This is generally recommended to reduce ambiguity and to
* avoid issues such as when a "." appears in the path for other reasons.
*/
private boolean useRegisteredSuffixPattern = false;
public boolean isUseSuffixPattern() {
return this.useSuffixPattern;
}
public void setUseSuffixPattern(boolean useSuffixPattern) {
this.useSuffixPattern = useSuffixPattern;
}
public boolean isUseRegisteredSuffixPattern() {
return this.useRegisteredSuffixPattern;
}
public void setUseRegisteredSuffixPattern(boolean useRegisteredSuffixPattern) {
this.useRegisteredSuffixPattern = useRegisteredSuffixPattern;
}
}
public enum LocaleResolver {
/**

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -62,6 +62,7 @@ import org.springframework.util.StringUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.filter.HttpPutFormContentFilter;
import org.springframework.web.servlet.HandlerAdapter;
@ -470,7 +471,8 @@ public class WebMvcAutoConfigurationTests {
@Test
public void customMediaTypes() {
this.contextRunner.withPropertyValues("spring.mvc.mediaTypes.yaml:text/yaml")
this.contextRunner.withPropertyValues("spring.mvc.mediaTypes.yaml:text/yaml",
"spring.mvc.content-negotiation.favor-path-extension:true")
.run((context) -> {
RequestMappingHandlerAdapter adapter = context
.getBean(RequestMappingHandlerAdapter.class);
@ -738,6 +740,63 @@ public class WebMvcAutoConfigurationTests {
.run((context) -> assertCacheControl(context));
}
@Test
public void defaultPathMatching() {
this.contextRunner.run((context) -> {
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
assertThat(handlerMapping.useSuffixPatternMatch()).isFalse();
assertThat(handlerMapping.useRegisteredSuffixPatternMatch()).isFalse();
});
}
@Test
public void useSuffixPatternMatch() {
this.contextRunner
.withPropertyValues("spring.mvc.path-match.use-suffix-pattern:true",
"spring.mvc.path-match.use-registered-suffix-pattern:true")
.run((context) -> {
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
assertThat(handlerMapping.useSuffixPatternMatch()).isTrue();
assertThat(handlerMapping.useRegisteredSuffixPatternMatch()).isTrue();
});
}
@Test
public void defaultContentNegotiation() {
this.contextRunner.run((context) -> {
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
ContentNegotiationManager contentNegotiationManager = handlerMapping.getContentNegotiationManager();
assertThat(contentNegotiationManager.getStrategies())
.doesNotHaveAnyElementsOfTypes(WebMvcAutoConfiguration
.OptionalPathExtensionContentNegotiationStrategy.class);
});
}
@Test
public void pathExtensionContentNegotiation() {
this.contextRunner
.withPropertyValues("spring.mvc.content-negotiation.favor-path-extension:true")
.run((context) -> {
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
ContentNegotiationManager contentNegotiationManager = handlerMapping.getContentNegotiationManager();
assertThat(contentNegotiationManager.getStrategies())
.hasAtLeastOneElementOfType(WebMvcAutoConfiguration
.OptionalPathExtensionContentNegotiationStrategy.class);
});
}
@Test
public void queryParameterContentNegotiation() {
this.contextRunner
.withPropertyValues("spring.mvc.content-negotiation.favor-parameter:true")
.run((context) -> {
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
ContentNegotiationManager contentNegotiationManager = handlerMapping.getContentNegotiationManager();
assertThat(contentNegotiationManager.getStrategies())
.hasAtLeastOneElementOfType(ParameterContentNegotiationStrategy.class);
});
}
private void assertCacheControl(AssertableWebApplicationContext context) {
Map<String, Object> handlerMap = getHandlerMap(
context.getBean("resourceHandlerMapping", HandlerMapping.class));

@ -393,6 +393,9 @@ content into your application. Rather, pick only the properties that you need.
# SPRING MVC ({sc-spring-boot-autoconfigure}/web/servlet/WebMvcProperties.{sc-ext}[WebMvcProperties])
spring.mvc.async.request-timeout= # Amount of time before asynchronous request handling times out.
spring.mvc.content-negotiation.favor-path-extension=false # Whether the path extension in the URL path should be used to determine the requested media type.
spring.mvc.content-negotiation.favor-parameter=false # Whether a request parameter ("format" by default) should be used to determine the requested media type.
spring.mvc.content-negotiation.parameter-name= # Query parameter name to use when "favor-parameter" is enabled.
spring.mvc.date-format= # Date format to use. For instance, `dd/MM/yyyy`.
spring.mvc.dispatch-trace-request=false # Whether to dispatch TRACE requests to the FrameworkServlet doService method.
spring.mvc.dispatch-options-request=true # Whether to dispatch OPTIONS requests to the FrameworkServlet doService method.
@ -404,6 +407,8 @@ content into your application. Rather, pick only the properties that you need.
spring.mvc.log-resolved-exception=false # Whether to enable warn logging of exceptions resolved by a "HandlerExceptionResolver".
spring.mvc.media-types.*= # Maps file extensions to media types for content negotiation.
spring.mvc.message-codes-resolver-format= # Formatting strategy for message codes. For instance, `PREFIX_ERROR_CODE`.
spring.mvc.path-match.use-registered-suffix-pattern=false # Whether suffix pattern matching should work only against path extensions explicitly registered with "spring.mvc.media-types.*".
spring.mvc.path-match.use-suffix-pattern=false # Whether to use suffix pattern match (".*") when matching patterns to requests.
spring.mvc.servlet.load-on-startup=-1 # Load on startup priority of the Spring Web Services servlet.
spring.mvc.static-path-pattern=/** # Path pattern used for static resources.
spring.mvc.throw-exception-if-no-handler-found=false # Whether a "NoHandlerFoundException" should be thrown if no Handler was found to process a request.

@ -2053,6 +2053,52 @@ root of the classpath (in that order). If such a file is present, it is automati
used as the favicon of the application.
[[boot-features-spring-mvc-pathmatch]]
==== Path Patching and Content Negotiation
Spring MVC can map incoming HTTP requests to handlers by looking at the request path and
matching it to the mappings defined in your application (for example, `@GetMapping`
annotations on Controller methods).
Spring Boot chooses to disable suffix pattern matching by default, which means that
requests like `"GET /projects/spring-boot.json"` won't be matched to
`@GetMapping("/project/spring-boot")` mappings.
This is considered as a
{spring-reference}web.html#mvc-ann-requestmapping-suffix-pattern-match[best practice
for Spring MVC applications]. This feature was mainly useful in the past for HTTP
clients which did not send proper "Accept" request headers; we needed to make sure
to send the correct Content Type to the client. Nowadays, Content Negotiation
is much more reliable.
There are other ways to deal with HTTP clients that don't consistently send proper
"Accept" request headers. Instead of using suffix matching, we can use a query
parameter to ensure that requests like `"GET /projects/spring-boot?format=json"`
will be mapped to `@GetMapping("/project/spring-boot")`:
[source,properties,indent=0,subs="verbatim,quotes,attributes"]
----
spring.mvc.content-negotiation.favor-parameter=true
# We can change the parameter name, which is "format" by default:
# spring.mvc.content-negotiation.parameter-name=myparam
# We can also register additional file extensions/media types with:
spring.mvc.media-types.markdown=text/markdown
----
If you understand the caveats and would still like your application to use
suffix pattern matching, the following configuration is required:
[source,properties,indent=0,subs="verbatim,quotes,attributes"]
----
spring.mvc.content-negotiation.favor-path-extension=true
# You can also restrict that feature to known extensions only
# spring.mvc.path-match.use-registered-suffix-pattern=true
# We can also register additional file extensions/media types with:
# spring.mvc.media-types.adoc=text/asciidoc
----
[[boot-features-spring-mvc-web-binding-initializer]]
==== ConfigurableWebBindingInitializer

Loading…
Cancel
Save