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:
Brian Clozel
2018-01-15 11:10:05 +01:00
parent 2bf662f231
commit 67e5897c40
5 changed files with 227 additions and 10 deletions

View File

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

View File

@@ -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 {
/**

View File

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