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
This commit is contained in:
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.ContentNegotiationConfigurer;
|
||||||
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
|
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
|
||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
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.ResourceChainRegistration;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
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
|
@Override
|
||||||
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
|
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();
|
Map<String, MediaType> mediaTypes = this.mvcProperties.getMediaTypes();
|
||||||
for (Entry<String, MediaType> mediaType : mediaTypes.entrySet()) {
|
for (Entry<String, MediaType> mediaType : mediaTypes.entrySet()) {
|
||||||
configurer.mediaType(mediaType.getKey(), mediaType.getValue());
|
configurer.mediaType(mediaType.getKey(), mediaType.getValue());
|
||||||
|
|
@ -450,8 +466,8 @@ public class WebMvcAutoConfiguration {
|
||||||
@Override
|
@Override
|
||||||
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
|
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
|
||||||
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
|
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
|
||||||
adapter.setIgnoreDefaultModelOnRedirect(this.mvcProperties == null ? true
|
adapter.setIgnoreDefaultModelOnRedirect(this.mvcProperties == null
|
||||||
: this.mvcProperties.isIgnoreDefaultModelOnRedirect());
|
|| this.mvcProperties.isIgnoreDefaultModelOnRedirect());
|
||||||
return adapter;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 Sébastien Deleuze
|
||||||
* @author Stephane Nicoll
|
* @author Stephane Nicoll
|
||||||
* @author Eddú Meléndez
|
* @author Eddú Meléndez
|
||||||
|
* @author Brian Clozel
|
||||||
* @since 1.1
|
* @since 1.1
|
||||||
*/
|
*/
|
||||||
@ConfigurationProperties(prefix = "spring.mvc")
|
@ConfigurationProperties(prefix = "spring.mvc")
|
||||||
|
|
@ -102,6 +103,10 @@ public class WebMvcProperties {
|
||||||
|
|
||||||
private final View view = new View();
|
private final View view = new View();
|
||||||
|
|
||||||
|
private final ContentNegotiation contentNegotiation = new ContentNegotiation();
|
||||||
|
|
||||||
|
private final PathMatch pathMatch = new PathMatch();
|
||||||
|
|
||||||
public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
|
public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
|
||||||
return this.messageCodesResolverFormat;
|
return this.messageCodesResolverFormat;
|
||||||
}
|
}
|
||||||
|
|
@ -204,6 +209,14 @@ public class WebMvcProperties {
|
||||||
return this.view;
|
return this.view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ContentNegotiation getContentNegotiation() {
|
||||||
|
return this.contentNegotiation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PathMatch getPathMatch() {
|
||||||
|
return this.pathMatch;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Async {
|
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 {
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Validator;
|
||||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||||
import org.springframework.web.accept.ContentNegotiationManager;
|
import org.springframework.web.accept.ContentNegotiationManager;
|
||||||
|
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
|
||||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||||
import org.springframework.web.filter.HttpPutFormContentFilter;
|
import org.springframework.web.filter.HttpPutFormContentFilter;
|
||||||
import org.springframework.web.servlet.HandlerAdapter;
|
import org.springframework.web.servlet.HandlerAdapter;
|
||||||
|
|
@ -470,7 +471,8 @@ public class WebMvcAutoConfigurationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void customMediaTypes() {
|
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) -> {
|
.run((context) -> {
|
||||||
RequestMappingHandlerAdapter adapter = context
|
RequestMappingHandlerAdapter adapter = context
|
||||||
.getBean(RequestMappingHandlerAdapter.class);
|
.getBean(RequestMappingHandlerAdapter.class);
|
||||||
|
|
@ -738,6 +740,63 @@ public class WebMvcAutoConfigurationTests {
|
||||||
.run((context) -> assertCacheControl(context));
|
.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) {
|
private void assertCacheControl(AssertableWebApplicationContext context) {
|
||||||
Map<String, Object> handlerMap = getHandlerMap(
|
Map<String, Object> handlerMap = getHandlerMap(
|
||||||
context.getBean("resourceHandlerMapping", HandlerMapping.class));
|
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 ({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.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.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-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.
|
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.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.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.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.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.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.
|
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.
|
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]]
|
[[boot-features-spring-mvc-web-binding-initializer]]
|
||||||
==== ConfigurableWebBindingInitializer
|
==== ConfigurableWebBindingInitializer
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue