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:
Phillip Webb
2019-08-30 14:27:19 -07:00
parent 587e116be8
commit 5938ca78b6
10 changed files with 248 additions and 57 deletions

View File

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

View File

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

View File

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

View File

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