Guard against WebServerApplicationContext not being present

Update security matchers and WebFlux actuator support to guard against
the `WebServerApplicationContext` class not being present.

Fixes gh-48388
This commit is contained in:
Phillip Webb
2025-12-03 22:41:32 -08:00
parent 1c637d8692
commit e93f9c313c
12 changed files with 162 additions and 40 deletions
+1
View File
@@ -27,6 +27,7 @@ description = "Spring Boot Actuator"
dependencies {
api(project(":core:spring-boot"))
optional(project(":module:spring-boot-web-server"))
optional("com.fasterxml.jackson.core:jackson-databind")
optional("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
optional("com.github.ben-manes.caffeine:caffeine")
@@ -18,6 +18,9 @@ package org.springframework.boot.actuate.endpoint.web;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -30,6 +33,8 @@ import org.springframework.util.StringUtils;
*/
public final class WebServerNamespace {
private static final String WEB_SERVER_CONTEXT_CLASS = "org.springframework.boot.web.server.context.WebServerApplicationContext";
/**
* {@link WebServerNamespace} that represents the main server.
*/
@@ -76,6 +81,21 @@ public final class WebServerNamespace {
return this.value;
}
/**
* Factory method to create a new {@link WebServerNamespace} from a value. If the
* context is {@code null} or not a web server context then {@link #SERVER} is
* returned.
* @param context the application context
* @return the web server namespace
* @since 4.0.1
*/
public static WebServerNamespace from(@Nullable ApplicationContext context) {
if (!ClassUtils.isPresent(WEB_SERVER_CONTEXT_CLASS, null)) {
return SERVER;
}
return from(WebServerApplicationContext.getServerNamespace(context));
}
/**
* Factory method to create a new {@link WebServerNamespace} from a value. If the
* value is empty or {@code null} then {@link #SERVER} is returned.
@@ -35,7 +35,7 @@ class WebServerNamespaceTests {
@Test
void fromWhenValueIsNull() {
assertThat(WebServerNamespace.from(null)).isEqualTo(WebServerNamespace.SERVER);
assertThat(WebServerNamespace.from((String) null)).isEqualTo(WebServerNamespace.SERVER);
}
@Test
@@ -38,7 +38,6 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.security.web.reactive.ApplicationContextServerWebExchangeMatcher;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
@@ -221,14 +220,14 @@ public final class EndpointRequest {
protected final boolean hasWebServerNamespace(@Nullable ApplicationContext applicationContext,
WebServerNamespace webServerNamespace) {
return WebServerApplicationContext.hasServerNamespace(applicationContext, webServerNamespace.getValue())
return hasServerNamespace(applicationContext, webServerNamespace.getValue())
|| hasImplicitServerNamespace(applicationContext, webServerNamespace);
}
private boolean hasImplicitServerNamespace(@Nullable ApplicationContext applicationContext,
WebServerNamespace webServerNamespace) {
return WebServerNamespace.SERVER.equals(webServerNamespace)
&& WebServerApplicationContext.getServerNamespace(applicationContext) == null
&& getServerNamespace(applicationContext) == null
&& getApplicationContextParent(applicationContext) == null;
}
@@ -38,7 +38,6 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.security.web.servlet.ApplicationContextRequestMatcher;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
@@ -183,15 +182,14 @@ public final class EndpointRequest {
protected final boolean hasWebServerNamespace(ApplicationContext applicationContext,
WebServerNamespace webServerNamespace) {
return WebServerApplicationContext.hasServerNamespace(applicationContext, webServerNamespace.getValue())
return hasServerNamespace(applicationContext, webServerNamespace.getValue())
|| hasImplicitServerNamespace(applicationContext, webServerNamespace);
}
private boolean hasImplicitServerNamespace(ApplicationContext applicationContext,
WebServerNamespace webServerNamespace) {
return WebServerNamespace.SERVER.equals(webServerNamespace)
&& WebServerApplicationContext.getServerNamespace(applicationContext) == null
&& applicationContext.getParent() == null;
&& getServerNamespace(applicationContext) == null && applicationContext.getParent() == null;
}
@Override
@@ -24,7 +24,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.boot.h2console.autoconfigure.H2ConsoleProperties;
import org.springframework.boot.security.autoconfigure.web.StaticResourceLocation;
import org.springframework.boot.security.web.servlet.ApplicationContextRequestMatcher;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -76,7 +75,7 @@ public final class PathRequest {
@Override
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) {
return WebServerApplicationContext.hasServerNamespace(applicationContext, "management");
return hasServerNamespace(applicationContext, "management");
}
@Override
@@ -27,7 +27,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.boot.security.autoconfigure.web.StaticResourceLocation;
import org.springframework.boot.security.web.servlet.ApplicationContextRequestMatcher;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
@@ -148,7 +147,7 @@ public final class StaticResourceRequest {
@Override
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) {
return WebServerApplicationContext.hasServerNamespace(applicationContext, "management");
return hasServerNamespace(applicationContext, "management");
}
@Override
@@ -22,9 +22,11 @@ import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.server.ServerWebExchange;
/**
@@ -41,6 +43,8 @@ import org.springframework.web.server.ServerWebExchange;
*/
public abstract class ApplicationContextServerWebExchangeMatcher<C> implements ServerWebExchangeMatcher {
private static final String WEB_SERVER_CONTEXT_CLASS = "org.springframework.boot.web.server.context.WebServerApplicationContext";
private final Class<? extends C> contextClass;
private volatile @Nullable Supplier<C> context;
@@ -111,4 +115,34 @@ public abstract class ApplicationContextServerWebExchangeMatcher<C> implements S
return () -> context.getBean(this.contextClass);
}
/**
* 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 4.0.1
*/
protected final boolean hasServerNamespace(@Nullable ApplicationContext context, String serverNamespace) {
if (!ClassUtils.isPresent(WEB_SERVER_CONTEXT_CLASS, null)) {
return false;
}
return WebServerApplicationContext.hasServerNamespace(context, serverNamespace);
}
/**
* Returns the server namespace if the specified context is a
* {@link WebServerApplicationContext}.
* @param context the context
* @return the server namespace or {@code null} if the context is not a
* {@link WebServerApplicationContext}
* @since 4.0.1
*/
protected final @Nullable String getServerNamespace(@Nullable ApplicationContext context) {
if (!ClassUtils.isPresent(WEB_SERVER_CONTEXT_CLASS, null)) {
return null;
}
return WebServerApplicationContext.getServerNamespace(context);
}
}
@@ -19,11 +19,14 @@ package org.springframework.boot.security.web.servlet;
import java.util.function.Supplier;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
@@ -41,6 +44,8 @@ import org.springframework.web.context.support.WebApplicationContextUtils;
*/
public abstract class ApplicationContextRequestMatcher<C> implements RequestMatcher {
private static final String WEB_SERVER_CONTEXT_CLASS = "org.springframework.boot.web.server.context.WebServerApplicationContext";
private final Class<? extends C> contextClass;
private volatile boolean initialized;
@@ -110,4 +115,34 @@ public abstract class ApplicationContextRequestMatcher<C> implements RequestMatc
*/
protected abstract boolean matches(HttpServletRequest request, Supplier<C> context);
/**
* 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 4.0.1
*/
protected final boolean hasServerNamespace(@Nullable ApplicationContext context, String serverNamespace) {
if (!ClassUtils.isPresent(WEB_SERVER_CONTEXT_CLASS, null)) {
return false;
}
return WebServerApplicationContext.hasServerNamespace(context, serverNamespace);
}
/**
* Returns the server namespace if the specified context is a
* {@link WebServerApplicationContext}.
* @param context the context
* @return the server namespace or {@code null} if the context is not a
* {@link WebServerApplicationContext}
* @since 4.0.1
*/
protected final @Nullable String getServerNamespace(@Nullable ApplicationContext context) {
if (!ClassUtils.isPresent(WEB_SERVER_CONTEXT_CLASS, null)) {
return null;
}
return WebServerApplicationContext.getServerNamespace(context);
}
}
@@ -22,14 +22,19 @@ import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.boot.h2console.autoconfigure.H2ConsoleProperties;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.StringUtils;
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.assertThatExceptionOfType;
/**
* Tests for {@link PathRequest}.
@@ -59,14 +64,31 @@ class PathRequestTests {
assertMatcher(matcher, "management").doesNotMatch("/js/file.js");
}
@Test
@ClassPathExclusions(packages = "org.springframework.boot.web.server.context")
void toH2ConsoleWhenNoWebServerContextClassPresent() {
assertThatExceptionOfType(NoClassDefFoundError.class)
.isThrownBy(() -> WebServerApplicationContext.class.getName());
RequestMatcher matcher = PathRequest.toH2Console();
StaticWebApplicationContext context = new StaticWebApplicationContext();
assertMatcher(matcher, context).matches("/h2-console");
assertMatcher(matcher, context).matches("/h2-console/subpath");
assertMatcher(matcher, context).doesNotMatch("/js/file.js");
}
private RequestMatcherAssert assertMatcher(RequestMatcher matcher) {
return assertMatcher(matcher, null);
return assertMatcher(matcher, (String) null);
}
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, @Nullable String serverNamespace) {
TestWebApplicationContext context = new TestWebApplicationContext(serverNamespace);
context.registerBean(ServerProperties.class);
context.registerBean(H2ConsoleProperties.class);
StaticWebApplicationContext context = new TestWebApplicationContext(serverNamespace);
return assertMatcher(matcher, context);
}
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, WebApplicationContext context) {
GenericApplicationContext genericContext = (GenericApplicationContext) context;
genericContext.registerBean(ServerProperties.class);
genericContext.registerBean(H2ConsoleProperties.class);
return assertThat(new RequestMatcherAssert(context, matcher));
}
@@ -52,6 +52,7 @@ import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicat
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.boot.webflux.actuate.endpoint.web.AbstractWebFluxEndpointHandlerMapping.AbstractWebFluxEndpointHandlerMappingRuntimeHints;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
@@ -372,19 +373,32 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
@Override
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, @Nullable Map<String, String> body) {
Map<String, Object> arguments = getArguments(exchange, body);
OperationArgumentResolver serverNamespaceArgumentResolver = OperationArgumentResolver
.of(WebServerNamespace.class, () -> WebServerNamespace
.from(WebServerApplicationContext.getServerNamespace(exchange.getApplicationContext())));
return this.securityContextSupplier.get()
.map((securityContext) -> new InvocationContext(securityContext, arguments,
serverNamespaceArgumentResolver,
new ProducibleOperationArgumentResolver(
() -> exchange.getRequest().getHeaders().get("Accept"))))
.flatMap((invocationContext) -> handleResult((Publisher<?>) this.invoker.invoke(invocationContext),
.map((securityContext) -> getInvocationContext(securityContext, exchange, body))
.flatMap((invocationContext) -> handleResult(invoke(invocationContext),
exchange.getRequest().getMethod()));
}
private InvocationContext getInvocationContext(SecurityContext securityContext, ServerWebExchange exchange,
@Nullable Map<String, String> body) {
Map<String, Object> arguments = getArguments(exchange, body);
OperationArgumentResolver serverNamespaceResolver = OperationArgumentResolver.of(WebServerNamespace.class,
() -> getServerNamespace(exchange));
OperationArgumentResolver producibleOperationResolver = new ProducibleOperationArgumentResolver(
() -> exchange.getRequest().getHeaders().get("Accept"));
return new InvocationContext(securityContext, arguments, serverNamespaceResolver,
producibleOperationResolver);
}
private WebServerNamespace getServerNamespace(ServerWebExchange exchange) {
ApplicationContext context = exchange.getApplicationContext();
return WebServerNamespace.from(WebServerApplicationContext.getServerNamespace(context));
}
private @Nullable Publisher<?> invoke(InvocationContext invocationContext) {
return (@Nullable Publisher<?>) this.invoker.invoke(invocationContext);
}
private Map<String, Object> getArguments(ServerWebExchange exchange, @Nullable Map<String, String> body) {
Map<String, Object> arguments = new LinkedHashMap<>(getTemplateVariables(exchange));
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
@@ -51,7 +51,6 @@ import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.boot.webmvc.actuate.endpoint.web.AbstractWebMvcEndpointHandlerMapping.AbstractWebMvcEndpointHandlerMappingRuntimeHints;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.http.HttpHeaders;
@@ -323,13 +322,9 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
public @Nullable Object handle(HttpServletRequest request,
@RequestBody(required = false) @Nullable Map<String, String> body) {
HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
Map<String, Object> arguments = getArguments(request, body);
try {
ServletSecurityContext securityContext = new ServletSecurityContext(request);
ProducibleOperationArgumentResolver producibleOperationArgumentResolver = new ProducibleOperationArgumentResolver(
() -> headers.get("Accept"));
InvocationContext invocationContext = new InvocationContext(securityContext, arguments,
serverNamespaceArgumentResolver(request), producibleOperationArgumentResolver);
InvocationContext invocationContext = getInvocationContext(request, body, headers, securityContext);
return handleResult(this.operation.invoke(invocationContext), HttpMethod.valueOf(request.getMethod()));
}
catch (InvalidEndpointRequestException ex) {
@@ -337,15 +332,21 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
}
}
private OperationArgumentResolver serverNamespaceArgumentResolver(HttpServletRequest request) {
if (ClassUtils.isPresent("org.springframework.boot.web.server.context.WebServerApplicationContext", null)) {
return OperationArgumentResolver.of(WebServerNamespace.class, () -> {
WebApplicationContext applicationContext = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
return WebServerNamespace.from(WebServerApplicationContext.getServerNamespace(applicationContext));
});
}
return OperationArgumentResolver.of(WebServerNamespace.class, () -> null);
private InvocationContext getInvocationContext(HttpServletRequest request, @Nullable Map<String, String> body,
HttpHeaders headers, ServletSecurityContext securityContext) {
Map<String, Object> arguments = getArguments(request, body);
OperationArgumentResolver serverNamespaceResolver = OperationArgumentResolver.of(WebServerNamespace.class,
() -> getServerNamespace(request));
ProducibleOperationArgumentResolver producibleOperationResolver = new ProducibleOperationArgumentResolver(
() -> headers.get("Accept"));
return new InvocationContext(securityContext, arguments, serverNamespaceResolver,
producibleOperationResolver);
}
private @Nullable WebServerNamespace getServerNamespace(HttpServletRequest request) {
WebApplicationContext context = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
return WebServerNamespace.from(context);
}
@Override