Enable PathPattern based matching for MVC actuators
Closes gh-24645
This commit is contained in:
parent
c83aac6cc8
commit
393081f2e6
|
@ -38,6 +38,7 @@ import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
|||
import org.springframework.boot.actuate.endpoint.web.Link;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
@ -64,7 +65,8 @@ class CloudFoundryWebEndpointServletHandlerMapping extends AbstractWebMvcEndpoin
|
|||
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
|
||||
CorsConfiguration corsConfiguration, CloudFoundrySecurityInterceptor securityInterceptor,
|
||||
EndpointLinksResolver linksResolver) {
|
||||
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, true);
|
||||
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, true,
|
||||
WebMvcAutoConfiguration.pathPatternParser);
|
||||
this.securityInterceptor = securityInterceptor;
|
||||
this.linksResolver = linksResolver;
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
@ -82,7 +83,7 @@ public class WebMvcEndpointManagementContextConfiguration {
|
|||
boolean shouldRegisterLinksMapping = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
|
||||
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
|
||||
corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
|
||||
shouldRegisterLinksMapping);
|
||||
shouldRegisterLinksMapping, WebMvcAutoConfiguration.pathPatternParser);
|
||||
}
|
||||
|
||||
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment,
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.web.EndpointServlet;
|
|||
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
|
||||
|
@ -56,6 +57,7 @@ import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
|
|||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.both;
|
||||
import static org.hamcrest.Matchers.hasKey;
|
||||
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||
|
@ -78,6 +80,16 @@ class WebMvcEndpointIntegrationTests {
|
|||
this.context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void webMvcEndpointHandlerMappingIsConfiguredWithPathPatternParser() {
|
||||
this.context = new AnnotationConfigServletWebApplicationContext();
|
||||
this.context.register(DefaultConfiguration.class);
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.refresh();
|
||||
WebMvcEndpointHandlerMapping handlerMapping = this.context.getBean(WebMvcEndpointHandlerMapping.class);
|
||||
assertThat(handlerMapping.getPatternParser()).isEqualTo(WebMvcAutoConfiguration.pathPatternParser);
|
||||
}
|
||||
|
||||
@Test
|
||||
void endpointsAreSecureByDefault() throws Exception {
|
||||
this.context = new AnnotationConfigServletWebApplicationContext();
|
||||
|
|
|
@ -67,7 +67,7 @@ import org.springframework.web.servlet.handler.MatchableHandlerMapping;
|
|||
import org.springframework.web.servlet.handler.RequestMatchResult;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* A custom {@link HandlerMapping} that makes {@link ExposableWebEndpoint web endpoints}
|
||||
|
@ -95,7 +95,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
|||
private final Method handleMethod = ReflectionUtils.findMethod(OperationHandler.class, "handle",
|
||||
HttpServletRequest.class, Map.class);
|
||||
|
||||
private static final RequestMappingInfo.BuilderConfiguration builderConfig = getBuilderConfig();
|
||||
private RequestMappingInfo.BuilderConfiguration builderConfig = new RequestMappingInfo.BuilderConfiguration();
|
||||
|
||||
/**
|
||||
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
|
||||
|
@ -123,14 +123,48 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
|||
public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
|
||||
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
|
||||
CorsConfiguration corsConfiguration, boolean shouldRegisterLinksMapping) {
|
||||
this(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, shouldRegisterLinksMapping, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code AbstractWebMvcEndpointHandlerMapping} that provides mappings
|
||||
* for the operations of the given endpoints.
|
||||
* @param endpointMapping the base mapping for all endpoints
|
||||
* @param endpoints the web endpoints
|
||||
* @param endpointMediaTypes media types consumed and produced by the endpoints
|
||||
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
|
||||
* @param shouldRegisterLinksMapping whether the links endpoint should be registered
|
||||
* @param pathPatternParser the path pattern parser
|
||||
*/
|
||||
public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
|
||||
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
|
||||
CorsConfiguration corsConfiguration, boolean shouldRegisterLinksMapping,
|
||||
PathPatternParser pathPatternParser) {
|
||||
this.endpointMapping = endpointMapping;
|
||||
this.endpoints = endpoints;
|
||||
this.endpointMediaTypes = endpointMediaTypes;
|
||||
this.corsConfiguration = corsConfiguration;
|
||||
this.shouldRegisterLinksMapping = shouldRegisterLinksMapping;
|
||||
setPatternParser(pathPatternParser);
|
||||
setOrder(-100);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public void afterPropertiesSet() {
|
||||
this.builderConfig = new RequestMappingInfo.BuilderConfiguration();
|
||||
if (getPatternParser() != null) {
|
||||
this.builderConfig.setPatternParser(getPatternParser());
|
||||
}
|
||||
else {
|
||||
this.builderConfig.setPathMatcher(null);
|
||||
this.builderConfig.setTrailingSlashMatch(true);
|
||||
this.builderConfig.setSuffixPatternMatch(false);
|
||||
|
||||
}
|
||||
super.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initHandlerMethods() {
|
||||
for (ExposableWebEndpoint endpoint : this.endpoints) {
|
||||
|
@ -151,7 +185,8 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
|||
|
||||
@Override
|
||||
public RequestMatchResult match(HttpServletRequest request, String pattern) {
|
||||
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(builderConfig).build();
|
||||
Assert.isNull(getPatternParser(), "This HandlerMapping uses PathPatterns.");
|
||||
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.builderConfig).build();
|
||||
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
|
||||
if (matchingInfo == null) {
|
||||
return null;
|
||||
|
@ -161,15 +196,6 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
|||
return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static RequestMappingInfo.BuilderConfiguration getBuilderConfig() {
|
||||
RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
|
||||
config.setPathMatcher(null);
|
||||
config.setSuffixPatternMatch(false);
|
||||
config.setTrailingSlashMatch(true);
|
||||
return config;
|
||||
}
|
||||
|
||||
private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOperation operation) {
|
||||
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
|
||||
String path = predicate.getPath();
|
||||
|
@ -202,7 +228,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
|||
}
|
||||
|
||||
private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) {
|
||||
return RequestMappingInfo.paths(this.endpointMapping.createSubPath(path))
|
||||
return RequestMappingInfo.paths(this.endpointMapping.createSubPath(path)).options(this.builderConfig)
|
||||
.methods(RequestMethod.valueOf(predicate.getHttpMethod().name()))
|
||||
.consumes(predicate.getConsumes().toArray(new String[0]))
|
||||
.produces(predicate.getProduces().toArray(new String[0])).build();
|
||||
|
@ -211,7 +237,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
|||
private void registerLinksMapping() {
|
||||
RequestMappingInfo mapping = RequestMappingInfo.paths(this.endpointMapping.createSubPath(""))
|
||||
.methods(RequestMethod.GET).produces(this.endpointMediaTypes.getProduced().toArray(new String[0]))
|
||||
.options(builderConfig).build();
|
||||
.options(this.builderConfig).build();
|
||||
LinksHandler linksHandler = getLinksHandler();
|
||||
registerMapping(mapping, linksHandler, ReflectionUtils.findMethod(linksHandler.getClass(), "links",
|
||||
HttpServletRequest.class, HttpServletResponse.class));
|
||||
|
@ -335,7 +361,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
|||
}
|
||||
|
||||
private Object getRemainingPathSegments(HttpServletRequest request) {
|
||||
String[] pathTokens = tokenize(request, UrlPathHelper.PATH_ATTRIBUTE, true);
|
||||
String[] pathTokens = tokenize(request, HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, true);
|
||||
String[] patternTokens = tokenize(request, HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, false);
|
||||
int numberOfRemainingPathSegments = pathTokens.length - patternTokens.length + 1;
|
||||
Assert.state(numberOfRemainingPathSegments >= 0, "Unable to extract remaining path segments");
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.springframework.boot.actuate.endpoint.web.Link;
|
|||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
|
||||
|
@ -62,6 +63,27 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM
|
|||
setOrder(-100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code WebMvcEndpointHandlerMapping} instance that provides mappings
|
||||
* for the given endpoints.
|
||||
* @param endpointMapping the base mapping for all endpoints
|
||||
* @param endpoints the web endpoints
|
||||
* @param endpointMediaTypes media types consumed and produced by the endpoints
|
||||
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
|
||||
* @param linksResolver resolver for determining links to available endpoints
|
||||
* @param shouldRegisterLinksMapping whether the links endpoint should be registered
|
||||
* @param pathPatternParser the path pattern parser
|
||||
*/
|
||||
public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection<ExposableWebEndpoint> endpoints,
|
||||
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
|
||||
EndpointLinksResolver linksResolver, boolean shouldRegisterLinksMapping,
|
||||
PathPatternParser pathPatternParser) {
|
||||
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, shouldRegisterLinksMapping,
|
||||
pathPatternParser);
|
||||
this.linksResolver = linksResolver;
|
||||
setOrder(-100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LinksHandler getLinksHandler() {
|
||||
return new WebMvcLinksHandler();
|
||||
|
|
|
@ -56,8 +56,11 @@ import org.springframework.util.StringUtils;
|
|||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.servlet.handler.RequestMatchResult;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Integration tests for web endpoints exposed using Spring MVC.
|
||||
|
@ -104,24 +107,37 @@ class MvcWebEndpointIntegrationTests
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchWhenPathPatternParserShouldThrowException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> getMatchResult("/spring/", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchWhenRequestHasTrailingSlashShouldNotBeNull() {
|
||||
assertThat(getMatchResult("/spring/")).isNotNull();
|
||||
assertThat(getMatchResult("/spring/", false)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchWhenRequestHasSuffixShouldBeNull() {
|
||||
assertThat(getMatchResult("/spring.do")).isNull();
|
||||
assertThat(getMatchResult("/spring.do", false)).isNull();
|
||||
}
|
||||
|
||||
private RequestMatchResult getMatchResult(String servletPath) {
|
||||
private RequestMatchResult getMatchResult(String servletPath, boolean isPatternParser) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setServletPath(servletPath);
|
||||
AnnotationConfigServletWebServerApplicationContext context = createApplicationContext();
|
||||
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
if (isPatternParser) {
|
||||
context.register(WebMvcConfiguration.class);
|
||||
}
|
||||
else {
|
||||
context.register(PathMatcherWebMvcConfiguration.class);
|
||||
}
|
||||
context.register(TestEndpointConfiguration.class);
|
||||
context.refresh();
|
||||
WebMvcEndpointHandlerMapping bean = context.getBean(WebMvcEndpointHandlerMapping.class);
|
||||
try {
|
||||
// Setup request attributes
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
// Trigger initLookupPath
|
||||
bean.getHandler(request);
|
||||
}
|
||||
|
@ -156,7 +172,35 @@ class MvcWebEndpointIntegrationTests
|
|||
String endpointPath = environment.getProperty("endpointPath");
|
||||
return new WebMvcEndpointHandlerMapping(new EndpointMapping(endpointPath),
|
||||
endpointDiscoverer.getEndpoints(), endpointMediaTypes, corsConfiguration,
|
||||
new EndpointLinksResolver(endpointDiscoverer.getEndpoints()), StringUtils.hasText(endpointPath));
|
||||
new EndpointLinksResolver(endpointDiscoverer.getEndpoints()), StringUtils.hasText(endpointPath),
|
||||
new PathPatternParser());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportAutoConfiguration({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
|
||||
ServletWebServerFactoryAutoConfiguration.class, WebMvcAutoConfiguration.class,
|
||||
DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class })
|
||||
static class PathMatcherWebMvcConfiguration {
|
||||
|
||||
@Bean
|
||||
TomcatServletWebServerFactory tomcat() {
|
||||
return new TomcatServletWebServerFactory(0);
|
||||
}
|
||||
|
||||
@Bean
|
||||
WebMvcEndpointHandlerMapping webEndpointHandlerMapping(Environment environment,
|
||||
WebEndpointDiscoverer endpointDiscoverer, EndpointMediaTypes endpointMediaTypes) {
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setAllowedOrigins(Arrays.asList("https://example.com"));
|
||||
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
|
||||
String endpointPath = environment.getProperty("endpointPath");
|
||||
WebMvcEndpointHandlerMapping handlerMapping = new WebMvcEndpointHandlerMapping(
|
||||
new EndpointMapping(endpointPath), endpointDiscoverer.getEndpoints(), endpointMediaTypes,
|
||||
corsConfiguration, new EndpointLinksResolver(endpointDiscoverer.getEndpoints()),
|
||||
StringUtils.hasText(endpointPath));
|
||||
return handlerMapping;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -158,6 +158,11 @@ public class WebMvcAutoConfiguration {
|
|||
*/
|
||||
public static final String DEFAULT_SUFFIX = "";
|
||||
|
||||
/**
|
||||
* Instance of {@link PathPatternParser} shared across MVC and actuator configuration.
|
||||
*/
|
||||
public static final PathPatternParser pathPatternParser = new PathPatternParser();
|
||||
|
||||
private static final String SERVLET_LOCATION = "/";
|
||||
|
||||
@Bean
|
||||
|
@ -246,7 +251,7 @@ public class WebMvcAutoConfiguration {
|
|||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
if (this.mvcProperties.getPathmatch()
|
||||
.getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) {
|
||||
configurer.setPatternParser(new PathPatternParser());
|
||||
configurer.setPatternParser(pathPatternParser);
|
||||
}
|
||||
configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
|
||||
configurer.setUseRegisteredSuffixPatternMatch(
|
||||
|
|
Loading…
Reference in New Issue