Merge branch '4.0.x'

Closes gh-50190
This commit is contained in:
Andy Wilkinson
2026-04-23 08:57:44 +01:00
4 changed files with 121 additions and 52 deletions
@@ -38,6 +38,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.web.cors.reactive.PreFlightRequestHandler;
import org.springframework.web.cors.reactive.PreFlightRequestWebFilter;
@@ -63,7 +64,10 @@ public final class ReactiveManagementWebSecurityAutoConfiguration {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, PreFlightRequestHandler handler) {
http.authorizeExchange((exchanges) -> {
exchanges.matchers(healthMatcher(), additionalHealthPathsMatcher()).permitAll();
if (ClassUtils.isPresent("org.springframework.boot.health.actuate.endpoint.HealthEndpoint",
getClass().getClassLoader())) {
exchanges.matchers(healthMatcher(), additionalHealthPathsMatcher()).permitAll();
}
exchanges.anyExchange().authenticated();
});
PreFlightRequestWebFilter filter = new PreFlightRequestWebFilter(handler);
@@ -59,13 +59,13 @@ public final class ManagementWebSecurityAutoConfiguration {
@Bean
@Order(SecurityFilterProperties.BASIC_AUTH_ORDER)
SecurityFilterChain managementSecurityFilterChain(Environment environment, HttpSecurity http) {
if (ClassUtils.isPresent("org.springframework.boot.health.actuate.endpoint.HealthEndpoint",
getClass().getClassLoader())) {
http.authorizeHttpRequests((requests) -> {
http.authorizeHttpRequests((requests) -> {
if (ClassUtils.isPresent("org.springframework.boot.health.actuate.endpoint.HealthEndpoint",
getClass().getClassLoader())) {
requests.requestMatchers(healthMatcher(), additionalHealthPathsMatcher()).permitAll();
requests.anyRequest().authenticated();
});
}
}
requests.anyRequest().authenticated();
});
if (ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", null)) {
http.cors(withDefaults());
}
@@ -36,6 +36,7 @@ import org.springframework.boot.health.autoconfigure.registry.HealthContributorR
import org.springframework.boot.security.autoconfigure.web.reactive.ReactiveWebSecurityAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -68,22 +69,26 @@ import static org.springframework.security.config.Customizer.withDefaults;
class ReactiveManagementWebSecurityAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class,
InfoEndpointAutoConfiguration.class, WebFluxAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, WebFluxAutoConfiguration.class,
EnvironmentEndpointAutoConfiguration.class, EndpointAutoConfiguration.class,
WebEndpointAutoConfiguration.class, ReactiveWebSecurityAutoConfiguration.class,
ReactiveManagementWebSecurityAutoConfiguration.class));
@Test
void permitAllForHealth() {
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class)
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withUserConfiguration(UserDetailsServiceConfiguration.class)
.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull());
}
@Test
void withAdditionalPathsOnSamePort() {
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class)
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withUserConfiguration(UserDetailsServiceConfiguration.class)
.withPropertyValues("management.endpoint.health.group.test1.include=*",
"management.endpoint.health.group.test2.include=*",
"management.endpoint.health.group.test1.additional-path=server:/check1",
@@ -97,7 +102,10 @@ class ReactiveManagementWebSecurityAutoConfigurationTests {
@Test
void withAdditionalPathsOnDifferentPort() {
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class)
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withUserConfiguration(UserDetailsServiceConfiguration.class)
.withPropertyValues("management.endpoint.health.group.test1.include=*",
"management.endpoint.health.group.test2.include=*",
"management.endpoint.health.group.test1.additional-path=server:/check1",
@@ -112,6 +120,19 @@ class ReactiveManagementWebSecurityAutoConfigurationTests {
@Test
void securesEverythingElse() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withUserConfiguration(UserDetailsServiceConfiguration.class)
.run((context) -> {
assertThat(getRequiredAuthenticateHeader(context, "/actuator").get(0)).contains("Basic realm=");
assertThat(getRequiredAuthenticateHeader(context, "/foo").toString()).contains("Basic realm=");
});
}
@Test
@ClassPathExclusions(packages = "org.springframework.boot.health.actuate.endpoint")
void securesEverythingElseWhenHealthIsAbsent() {
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class).run((context) -> {
assertThat(getRequiredAuthenticateHeader(context, "/actuator").get(0)).contains("Basic realm=");
assertThat(getRequiredAuthenticateHeader(context, "/foo").toString()).contains("Basic realm=");
@@ -120,16 +141,22 @@ class ReactiveManagementWebSecurityAutoConfigurationTests {
@Test
void noExistingAuthenticationManagerOrUserDetailsService() {
this.contextRunner.run((context) -> {
assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull();
assertThat(getRequiredAuthenticateHeader(context, "/actuator").get(0)).contains("Basic realm=");
assertThat(getRequiredAuthenticateHeader(context, "/foo").toString()).contains("Basic realm=");
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.run((context) -> {
assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull();
assertThat(getRequiredAuthenticateHeader(context, "/actuator").get(0)).contains("Basic realm=");
assertThat(getRequiredAuthenticateHeader(context, "/foo").toString()).contains("Basic realm=");
});
}
@Test
void usesMatchersBasedOffConfiguredActuatorBasePath() {
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class)
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withUserConfiguration(UserDetailsServiceConfiguration.class)
.withPropertyValues("management.endpoints.web.base-path=/")
.run((context) -> {
assertThat(getAuthenticateHeader(context, "/health")).isNull();
@@ -139,18 +166,26 @@ class ReactiveManagementWebSecurityAutoConfigurationTests {
@Test
void backsOffIfCustomSecurityIsAdded() {
this.contextRunner.withUserConfiguration(CustomSecurityConfiguration.class).run((context) -> {
assertThat(getRequiredLocationHeader(context, "/actuator/health").toString()).contains("/login");
assertThat(getLocationHeader(context, "/foo")).isNull();
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withUserConfiguration(CustomSecurityConfiguration.class)
.run((context) -> {
assertThat(getRequiredLocationHeader(context, "/actuator/health").toString()).contains("/login");
assertThat(getLocationHeader(context, "/foo")).isNull();
});
}
@Test
void backsOffWhenWebFilterChainProxyBeanPresent() {
this.contextRunner.withUserConfiguration(WebFilterChainProxyConfiguration.class).run((context) -> {
assertThat(getRequiredLocationHeader(context, "/actuator/health").toString()).contains("/login");
assertThat(getRequiredLocationHeader(context, "/foo").toString()).contains("/login");
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withUserConfiguration(WebFilterChainProxyConfiguration.class)
.run((context) -> {
assertThat(getRequiredLocationHeader(context, "/actuator/health").toString()).contains("/login");
assertThat(getRequiredLocationHeader(context, "/foo").toString()).contains("/login");
});
}
private @Nullable List<String> getAuthenticateHeader(AssertableReactiveWebApplicationContext context, String path) {
@@ -37,6 +37,7 @@ import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSec
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
@@ -71,12 +72,10 @@ class ManagementWebSecurityAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(contextSupplier(),
WebServerApplicationContext.class)
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class,
InfoEndpointAutoConfiguration.class, EnvironmentEndpointAutoConfiguration.class,
EndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, WebEndpointAutoConfiguration.class,
SecurityAutoConfiguration.class, ServletWebSecurityAutoConfiguration.class,
ManagementWebSecurityAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class,
EnvironmentEndpointAutoConfiguration.class, EndpointAutoConfiguration.class,
WebMvcAutoConfiguration.class, WebEndpointAutoConfiguration.class, SecurityAutoConfiguration.class,
ServletWebSecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class));
private static Supplier<ConfigurableWebApplicationContext> contextSupplier() {
return WebApplicationContextRunner.withMockServletContext(MockWebServerApplicationContext::new);
@@ -84,15 +83,32 @@ class ManagementWebSecurityAutoConfigurationTests {
@Test
void permitAllForHealth() {
this.contextRunner.run((context) -> {
assertThat(context).hasBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN);
HttpStatus status = getResponseStatus(context, "/actuator/health");
assertThat(status).isEqualTo(HttpStatus.OK);
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN);
HttpStatus status = getResponseStatus(context, "/actuator/health");
assertThat(status).isEqualTo(HttpStatus.OK);
});
}
@Test
void securesEverythingElse() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.run((context) -> {
HttpStatus status = getResponseStatus(context, "/actuator");
assertThat(status).isEqualTo(HttpStatus.UNAUTHORIZED);
status = getResponseStatus(context, "/foo");
assertThat(status).isEqualTo(HttpStatus.UNAUTHORIZED);
});
}
@Test
@ClassPathExclusions(packages = "org.springframework.boot.health.actuate.endpoint")
void securesEverythingElseWhenHealthIsAbsent() {
this.contextRunner.run((context) -> {
HttpStatus status = getResponseStatus(context, "/actuator");
assertThat(status).isEqualTo(HttpStatus.UNAUTHORIZED);
@@ -112,20 +128,28 @@ class ManagementWebSecurityAutoConfigurationTests {
@Test
void usesMatchersBasedOffConfiguredActuatorBasePath() {
this.contextRunner.withPropertyValues("management.endpoints.web.base-path=/").run((context) -> {
HttpStatus status = getResponseStatus(context, "/health");
assertThat(status).isEqualTo(HttpStatus.OK);
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withPropertyValues("management.endpoints.web.base-path=/")
.run((context) -> {
HttpStatus status = getResponseStatus(context, "/health");
assertThat(status).isEqualTo(HttpStatus.OK);
});
}
@Test
void backOffIfCustomSecurityIsAdded() {
this.contextRunner.withUserConfiguration(CustomSecurityConfiguration.class).run((context) -> {
HttpStatus status = getResponseStatus(context, "/actuator/health");
assertThat(status).isEqualTo(HttpStatus.UNAUTHORIZED);
status = getResponseStatus(context, "/foo");
assertThat(status).isEqualTo(HttpStatus.OK);
});
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withUserConfiguration(CustomSecurityConfiguration.class)
.run((context) -> {
HttpStatus status = getResponseStatus(context, "/actuator/health");
assertThat(status).isEqualTo(HttpStatus.UNAUTHORIZED);
status = getResponseStatus(context, "/foo");
assertThat(status).isEqualTo(HttpStatus.OK);
});
}
@Test
@@ -155,6 +179,8 @@ class ManagementWebSecurityAutoConfigurationTests {
@Test
void withAdditionalPathsOnSamePort() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withPropertyValues("management.endpoint.health.group.test1.include=*",
"management.endpoint.health.group.test2.include=*",
"management.endpoint.health.group.test1.additional-path=server:/check1",
@@ -168,10 +194,14 @@ class ManagementWebSecurityAutoConfigurationTests {
@Test
void withAdditionalPathsOnDifferentPort() {
this.contextRunner.withPropertyValues("management.endpoint.health.group.test1.include=*",
"management.endpoint.health.group.test2.include=*",
"management.endpoint.health.group.test1.additional-path=server:/check1",
"management.endpoint.health.group.test2.additional-path=management:/check2", "management.server.port=0")
this.contextRunner
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class))
.withPropertyValues("management.endpoint.health.group.test1.include=*",
"management.endpoint.health.group.test2.include=*",
"management.endpoint.health.group.test1.additional-path=server:/check1",
"management.endpoint.health.group.test2.additional-path=management:/check2",
"management.server.port=0")
.run((context) -> {
assertThat(getResponseStatus(context, "/check1")).isEqualTo(HttpStatus.OK);
assertThat(getResponseStatus(context, "/check2")).isEqualTo(HttpStatus.UNAUTHORIZED);