Fix request matcher management context support
Fix caching issues in `ApplicationContextRequestMatcher` and allow subclasses to ignore an application context entirely. Update existing matcher implementations so that they deal with the management context correctly. Prior to this commit, the `ApplicationContextRequestMatcher` would return a context cached from the first request. It also didn't provide any way to ignore a context. This meant that if the user was running the management server on a different port the matching results could be inconsistent depending on if the first request arrived on the regular context or the management context. It also meant that we could not distinguish between the regular context and the management context when matching. Closes gh-18012
This commit is contained in:
parent
587e116be8
commit
5938ca78b6
|
@ -37,6 +37,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
|||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
|
||||
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
|
||||
import org.springframework.boot.web.context.WebServerApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
|
@ -44,7 +45,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
|
|||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.WebApplicationContextUtils;
|
||||
|
||||
/**
|
||||
* Factory that can be used to create a {@link RequestMatcher} for actuator endpoint
|
||||
|
@ -127,6 +127,13 @@ public final class EndpointRequest {
|
|||
super(WebApplicationContext.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) {
|
||||
ManagementPortType type = ManagementPortType.get(applicationContext.getEnvironment());
|
||||
return type == ManagementPortType.DIFFERENT
|
||||
&& WebServerApplicationContext.hasServerNamespace(applicationContext, "management");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void initialized(Supplier<WebApplicationContext> context) {
|
||||
this.delegate = createDelegate(context.get());
|
||||
|
@ -134,17 +141,6 @@ public final class EndpointRequest {
|
|||
|
||||
@Override
|
||||
protected final boolean matches(HttpServletRequest request, Supplier<WebApplicationContext> context) {
|
||||
WebApplicationContext applicationContext = WebApplicationContextUtils
|
||||
.getRequiredWebApplicationContext(request.getServletContext());
|
||||
if (ManagementPortType.get(applicationContext.getEnvironment()) == ManagementPortType.DIFFERENT) {
|
||||
if (applicationContext.getParent() == null) {
|
||||
return false;
|
||||
}
|
||||
String managementContextId = applicationContext.getParent().getId() + ":management";
|
||||
if (!managementContextId.equals(applicationContext.getId())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this.delegate.matches(request);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,8 +23,10 @@ import javax.servlet.http.HttpServletRequest;
|
|||
import org.springframework.boot.autoconfigure.h2.H2ConsoleProperties;
|
||||
import org.springframework.boot.autoconfigure.security.StaticResourceLocation;
|
||||
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
|
||||
import org.springframework.boot.web.context.WebServerApplicationContext;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* Factory that can be used to create a {@link RequestMatcher} for commonly used paths.
|
||||
|
@ -69,6 +71,11 @@ public final class PathRequest {
|
|||
super(H2ConsoleProperties.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) {
|
||||
return WebServerApplicationContext.hasServerNamespace(applicationContext, "management");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialized(Supplier<H2ConsoleProperties> h2ConsoleProperties) {
|
||||
this.delegate = new AntPathRequestMatcher(h2ConsoleProperties.get().getPath() + "/**");
|
||||
|
|
|
@ -29,10 +29,12 @@ import javax.servlet.http.HttpServletRequest;
|
|||
import org.springframework.boot.autoconfigure.security.StaticResourceLocation;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
|
||||
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
|
||||
import org.springframework.boot.web.context.WebServerApplicationContext;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* Used to create a {@link RequestMatcher} for static resources in commonly used
|
||||
|
@ -144,6 +146,11 @@ public final class StaticResourceRequest {
|
|||
.map(dispatcherServletPath::getRelativePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) {
|
||||
return WebServerApplicationContext.hasServerNamespace(applicationContext, "management");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matches(HttpServletRequest request, Supplier<DispatcherServletPath> context) {
|
||||
return this.delegate.matches(request);
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.springframework.mock.web.MockHttpServletRequest;
|
|||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
@ -51,8 +50,20 @@ public class PathRequestTests {
|
|||
assertMatcher(matcher).doesNotMatch("/js/file.js");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toH2ConsoleWhenManagementContextShouldNeverMatch() {
|
||||
RequestMatcher matcher = PathRequest.toH2Console();
|
||||
assertMatcher(matcher, "management").doesNotMatch("/h2-console");
|
||||
assertMatcher(matcher, "management").doesNotMatch("/h2-console/subpath");
|
||||
assertMatcher(matcher, "management").doesNotMatch("/js/file.js");
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher) {
|
||||
StaticWebApplicationContext context = new StaticWebApplicationContext();
|
||||
return assertMatcher(matcher, null);
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String serverNamespace) {
|
||||
TestWebApplicationContext context = new TestWebApplicationContext(serverNamespace);
|
||||
context.registerBean(ServerProperties.class);
|
||||
context.registerBean(H2ConsoleProperties.class);
|
||||
return assertThat(new RequestMatcherAssert(context, matcher));
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.springframework.mock.web.MockHttpServletRequest;
|
|||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
@ -53,6 +52,16 @@ public class StaticResourceRequestTests {
|
|||
assertMatcher(matcher).doesNotMatch("/bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atCommonLocationsWhenManagementContextShouldNeverMatch() {
|
||||
RequestMatcher matcher = this.resourceRequest.atCommonLocations();
|
||||
assertMatcher(matcher, "management").doesNotMatch("/css/file.css");
|
||||
assertMatcher(matcher, "management").doesNotMatch("/js/file.js");
|
||||
assertMatcher(matcher, "management").doesNotMatch("/images/file.css");
|
||||
assertMatcher(matcher, "management").doesNotMatch("/webjars/file.css");
|
||||
assertMatcher(matcher, "management").doesNotMatch("/foo/favicon.ico");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atCommonLocationsWithExcludeShouldNotMatchExcluded() {
|
||||
RequestMatcher matcher = this.resourceRequest.atCommonLocations().excluding(StaticResourceLocation.CSS);
|
||||
|
@ -70,8 +79,8 @@ public class StaticResourceRequestTests {
|
|||
@Test
|
||||
public void atLocationWhenHasServletPathShouldMatchLocation() {
|
||||
RequestMatcher matcher = this.resourceRequest.at(StaticResourceLocation.CSS);
|
||||
assertMatcher(matcher, "/foo").matches("/foo", "/css/file.css");
|
||||
assertMatcher(matcher, "/foo").doesNotMatch("/foo", "/js/file.js");
|
||||
assertMatcher(matcher, null, "/foo").matches("/foo", "/css/file.css");
|
||||
assertMatcher(matcher, null, "/foo").doesNotMatch("/foo", "/js/file.js");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -87,15 +96,16 @@ public class StaticResourceRequestTests {
|
|||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher) {
|
||||
DispatcherServletPath dispatcherServletPath = () -> "";
|
||||
StaticWebApplicationContext context = new StaticWebApplicationContext();
|
||||
context.registerBean(DispatcherServletPath.class, () -> dispatcherServletPath);
|
||||
return assertThat(new RequestMatcherAssert(context, matcher));
|
||||
return assertMatcher(matcher, null, "");
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String path) {
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String serverNamespace) {
|
||||
return assertMatcher(matcher, serverNamespace, "");
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String serverNamespace, String path) {
|
||||
DispatcherServletPath dispatcherServletPath = () -> path;
|
||||
StaticWebApplicationContext context = new StaticWebApplicationContext();
|
||||
TestWebApplicationContext context = new TestWebApplicationContext(serverNamespace);
|
||||
context.registerBean(DispatcherServletPath.class, () -> dispatcherServletPath);
|
||||
return assertThat(new RequestMatcherAssert(context, matcher));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2012-2019 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.security.servlet;
|
||||
|
||||
import org.springframework.boot.web.context.WebServerApplicationContext;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
|
||||
/**
|
||||
* Test {@link StaticWebApplicationContext} that also implements
|
||||
* {@link WebServerApplicationContext}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TestWebApplicationContext extends StaticWebApplicationContext implements WebServerApplicationContext {
|
||||
|
||||
private final String serverNamespace;
|
||||
|
||||
TestWebApplicationContext(String serverNamespace) {
|
||||
this.serverNamespace = serverNamespace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebServer getWebServer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerNamespace() {
|
||||
return this.serverNamespace;
|
||||
}
|
||||
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.boot.security.servlet;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
@ -43,9 +44,7 @@ public abstract class ApplicationContextRequestMatcher<C> implements RequestMatc
|
|||
|
||||
private final Class<? extends C> contextClass;
|
||||
|
||||
private volatile Supplier<C> context;
|
||||
|
||||
private final Object contextLock = new Object();
|
||||
private final AtomicBoolean initialized = new AtomicBoolean(false);
|
||||
|
||||
public ApplicationContextRequestMatcher(Class<? extends C> contextClass) {
|
||||
Assert.notNull(contextClass, "Context class must not be null");
|
||||
|
@ -54,7 +53,48 @@ public abstract class ApplicationContextRequestMatcher<C> implements RequestMatc
|
|||
|
||||
@Override
|
||||
public final boolean matches(HttpServletRequest request) {
|
||||
return matches(request, getContext(request));
|
||||
WebApplicationContext webApplicationContext = WebApplicationContextUtils
|
||||
.getRequiredWebApplicationContext(request.getServletContext());
|
||||
if (ignoreApplicationContext(webApplicationContext)) {
|
||||
return false;
|
||||
}
|
||||
Supplier<C> context = () -> getContext(webApplicationContext);
|
||||
if (this.initialized.compareAndSet(false, true)) {
|
||||
initialized(context);
|
||||
}
|
||||
return matches(request, context);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private C getContext(WebApplicationContext webApplicationContext) {
|
||||
if (this.contextClass.isInstance(webApplicationContext)) {
|
||||
return (C) webApplicationContext;
|
||||
}
|
||||
return webApplicationContext.getBean(this.contextClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the {@link WebApplicationContext} should be ignored and not used for
|
||||
* matching. If this method returns {@code true} then the context will not be used and
|
||||
* the {@link #matches(HttpServletRequest) matches} method will return {@code false}.
|
||||
* @param webApplicationContext the candidate web application context
|
||||
* @return if the application context should be ignored
|
||||
* @since 2.1.8
|
||||
*/
|
||||
protected boolean ignoreApplicationContext(WebApplicationContext webApplicationContext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that can be implemented by subclasses that wish to initialize items the
|
||||
* first time that the matcher is called. This method will be called only once and
|
||||
* only if {@link #ignoreApplicationContext(WebApplicationContext)} returns
|
||||
* {@code true}. Note that the supplied context will be based on the
|
||||
* <strong>first</strong> request sent to the matcher.
|
||||
* @param context a supplier for the initialized context (may throw an exception)
|
||||
* @see #ignoreApplicationContext(WebApplicationContext)
|
||||
*/
|
||||
protected void initialized(Supplier<C> context) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,34 +105,4 @@ public abstract class ApplicationContextRequestMatcher<C> implements RequestMatc
|
|||
*/
|
||||
protected abstract boolean matches(HttpServletRequest request, Supplier<C> context);
|
||||
|
||||
private Supplier<C> getContext(HttpServletRequest request) {
|
||||
if (this.context == null) {
|
||||
synchronized (this.contextLock) {
|
||||
if (this.context == null) {
|
||||
Supplier<C> createdContext = createContext(request);
|
||||
initialized(createdContext);
|
||||
this.context = createdContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once the context has been initialized.
|
||||
* @param context a supplier for the initialized context (may throw an exception)
|
||||
*/
|
||||
protected void initialized(Supplier<C> context) {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Supplier<C> createContext(HttpServletRequest request) {
|
||||
WebApplicationContext context = WebApplicationContextUtils
|
||||
.getRequiredWebApplicationContext(request.getServletContext());
|
||||
if (this.contextClass.isInstance(context)) {
|
||||
return () -> (C) context;
|
||||
}
|
||||
return () -> context.getBean(this.contextClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.boot.web.context;
|
|||
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by {@link ApplicationContext application contexts} that
|
||||
|
@ -44,4 +45,17 @@ public interface WebServerApplicationContext extends ApplicationContext {
|
|||
*/
|
||||
String getServerNamespace();
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the specified context is a
|
||||
* {@link WebServerApplicationContext} with a matching server namespace.
|
||||
* @param context the context to check
|
||||
* @param serverNamespace the server namespace to match against
|
||||
* @return {@code true} if the server namespace of the context matches
|
||||
* @since 2.1.8
|
||||
*/
|
||||
static boolean hasServerNamespace(ApplicationContext context, String serverNamespace) {
|
||||
return (context instanceof WebServerApplicationContext) && ObjectUtils
|
||||
.nullSafeEquals(((WebServerApplicationContext) context).getServerNamespace(), serverNamespace);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -69,6 +69,42 @@ public class ApplicationContextRequestMatcherTests {
|
|||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(supplier::get);
|
||||
}
|
||||
|
||||
@Test // gh-18012
|
||||
public void machesWhenCalledWithDifferentApplicationContextDoesNotCache() {
|
||||
StaticWebApplicationContext context1 = createWebApplicationContext();
|
||||
StaticWebApplicationContext context2 = createWebApplicationContext();
|
||||
TestApplicationContextRequestMatcher<ApplicationContext> matcher = new TestApplicationContextRequestMatcher<>(
|
||||
ApplicationContext.class);
|
||||
assertThat(matcher.callMatchesAndReturnProvidedContext(context1).get()).isEqualTo(context1);
|
||||
assertThat(matcher.callMatchesAndReturnProvidedContext(context2).get()).isEqualTo(context2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeAndMatchesAreNotCalledIfContextIsIgnored() {
|
||||
StaticWebApplicationContext context = createWebApplicationContext();
|
||||
TestApplicationContextRequestMatcher<ApplicationContext> matcher = new TestApplicationContextRequestMatcher<ApplicationContext>(
|
||||
ApplicationContext.class) {
|
||||
|
||||
@Override
|
||||
protected boolean ignoreApplicationContext(WebApplicationContext webApplicationContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialized(Supplier<ApplicationContext> context) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matches(HttpServletRequest request, Supplier<ApplicationContext> context) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
};
|
||||
MockHttpServletRequest request = new MockHttpServletRequest(context.getServletContext());
|
||||
assertThat(matcher.matches(request)).isFalse();
|
||||
}
|
||||
|
||||
private StaticWebApplicationContext createWebApplicationContext() {
|
||||
StaticWebApplicationContext context = new StaticWebApplicationContext();
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2012-2019 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.context;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebServerApplicationContext}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class WebServerApplicationContextTests {
|
||||
|
||||
@Test
|
||||
public void hasServerNamespaceWhenContextIsNotWebServerApplicationContextReturnsFalse() {
|
||||
ApplicationContext context = mock(ApplicationContext.class);
|
||||
assertThat(WebServerApplicationContext.hasServerNamespace(context, "test")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasServerNamespaceWhenContextIsWebServerApplicationContextAndNamespaceDoesNotMatchReturnsFalse() {
|
||||
ApplicationContext context = mock(WebServerApplicationContext.class);
|
||||
assertThat(WebServerApplicationContext.hasServerNamespace(context, "test")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasServerNamespaceWhenContextIsWebServerApplicationContextAndNamespaceMatchesReturnsTrue() {
|
||||
WebServerApplicationContext context = mock(WebServerApplicationContext.class);
|
||||
given(context.getServerNamespace()).willReturn("test");
|
||||
assertThat(WebServerApplicationContext.hasServerNamespace(context, "test")).isTrue();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue