Browse Source

Align reactive web security more closely with servlet web security

There are some notable differences in the behavior of Spring
Security's reactive and servlet-based web security. Notably,
Servlet-based web security (`@EnableWebSecurity`) works without
any authentication manager, rejecting requests as not authorized.
By contrast reactive-based web security (`@EnableWebFluxSecurity`)
fails to start up when there's no authentication manager, either
provided directly as a bean or derived from a
ReactiveUserDetailsService. There are also further differences at
runtime where empty Monos from all ReactiveAuthenticationManagers
results in an internal error and a 500 response whereas a similar
situation in the servlet implementation results in a 401.

Previously, to accommodate these differences in behavior, Spring
Boot's auto-configuration would behave differently. In the Servlet
case, web security would be enabled whenever the necessary
dependencies were on the classpath. In the reactive case, web
security would back off in the absence of an authentication manager
to prevent a start up failure. While this difference is rooted in
Spring Security, it is undesirable and something that we want to
avoid Spring Boot users being exposed to where possible.
Unfortunately, the situation is more likely to occur than before
as ReactiveUserDetailsServiceAutoConfiguration now backs off more
readily (gh-35338). This makes it more likely that the context will
contain neither a reactive authetication manager not a reactive
user details service.

This commit reworks the auto-configurations related to reactive
security. ReactiveSecurityAutoConfiguration will now auto-configure
an "empty" reactive authentication manager that denies access through
Mono.error in the absence of a ReactiveAuthenticationManager,
ReactiveUserDetailsService, or SecurityWebFilterChain. The last of
these is to allow for the situation where a filter chain has been
defined with an authentication manager configured directly on it.
This configuration of an authentication manager allows
`@EnableWebFluxSecurity` to be auto-configured more readily,
removing one of the differences between reactive- and Servlet-based
security.

Corresponding updates to the auto-configurations for reactive OAuth2
support have also been made. They no longer try to auto-configure
`@EnableWebFluxSecurity`, relying instead upon
ReactiveSecurityAutoConfiguration, which they are ordered before, to
do that instead.

Closes gh-38713
pull/38771/head
Andy Wilkinson 2 years ago
parent
commit
afad358047
  1. 9
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java
  2. 9
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
  3. 9
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java
  4. 40
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java
  5. 2
      spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
  6. 21
      spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java

9
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java

@ -28,7 +28,6 @@ import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2Clien @@ -28,7 +28,6 @@ import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2Clien
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
@ -38,7 +37,6 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg @@ -38,7 +37,6 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import static org.springframework.security.config.Customizer.withDefaults;
@ -94,13 +92,6 @@ class ReactiveOAuth2ClientConfigurations { @@ -94,13 +92,6 @@ class ReactiveOAuth2ClientConfigurations {
return http.build();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebFilterChainProxy.class)
@EnableWebFluxSecurity
static class EnableWebFluxSecurityConfiguration {
}
}
}

9
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java

@ -35,7 +35,6 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2Res @@ -35,7 +35,6 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2Res
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
@ -50,7 +49,6 @@ import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUr @@ -50,7 +49,6 @@ import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUr
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.util.CollectionUtils;
/**
@ -179,13 +177,6 @@ class ReactiveOAuth2ResourceServerJwkConfiguration { @@ -179,13 +177,6 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
server.jwt((jwt) -> jwt.jwtDecoder(decoder));
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebFilterChainProxy.class)
@EnableWebFluxSecurity
static class EnableWebFluxSecurityConfiguration {
}
}
}

9
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java

@ -22,12 +22,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -22,12 +22,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import static org.springframework.security.config.Customizer.withDefaults;
@ -66,13 +64,6 @@ class ReactiveOAuth2ResourceServerOpaqueTokenConfiguration { @@ -66,13 +64,6 @@ class ReactiveOAuth2ResourceServerOpaqueTokenConfiguration {
return http.build();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebFilterChainProxy.class)
@EnableWebFluxSecurity
static class EnableWebFluxSecurityConfiguration {
}
}
}

40
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java

@ -17,21 +17,21 @@ @@ -17,21 +17,21 @@
package org.springframework.boot.autoconfigure.security.reactive;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
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.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@ -52,33 +52,21 @@ import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -52,33 +52,21 @@ import org.springframework.web.reactive.config.WebFluxConfigurer;
@ConditionalOnClass({ Flux.class, EnableWebFluxSecurity.class, WebFilterChainProxy.class, WebFluxConfigurer.class })
public class ReactiveSecurityAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebFilterChainProxy.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@Conditional(EnableWebFluxSecurityCondition.class)
@EnableWebFluxSecurity
static class EnableWebFluxSecurityConfiguration {
}
static final class EnableWebFluxSecurityCondition extends AnyNestedCondition {
EnableWebFluxSecurityCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnBean(ReactiveAuthenticationManager.class)
static final class ConditionalOnReactiveAuthenticationManagerBean {
}
@ConditionalOnBean(ReactiveUserDetailsService.class)
static final class ConditionalOnReactiveUserDetailsService {
@Configuration(proxyBeanMethods = false)
class SpringBootWebFluxSecurityConfiguration {
@Bean
@ConditionalOnMissingBean({ ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class,
SecurityWebFilterChain.class })
ReactiveAuthenticationManager denyAllAuthenticationManager() {
return (authentication) -> Mono.error(new UsernameNotFoundException(authentication.getName()));
}
@ConditionalOnBean(SecurityWebFilterChain.class)
static final class ConditionalOnSecurityWebFilterChain {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebFilterChainProxy.class)
@EnableWebFluxSecurity
static class EnableWebFluxSecurityConfiguration {
}

2
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java

@ -740,6 +740,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { @@ -740,6 +740,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
.isEqualTo("aud");
}
@EnableWebFluxSecurity
static class TestConfig {
@Bean
@ -781,7 +782,6 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { @@ -781,7 +782,6 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
}
@EnableWebFluxSecurity
@Configuration(proxyBeanMethods = false)
static class SecurityWebFilterChainConfig {

21
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java

@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test; @@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.EnableWebFluxSecurityConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
@ -53,28 +52,36 @@ class ReactiveSecurityAutoConfigurationTests { @@ -53,28 +52,36 @@ class ReactiveSecurityAutoConfigurationTests {
}
@Test
void backsOffWhenReactiveAuthenticationManagerNotPresent() {
void autoConfiguresDenyAllReactiveAuthenticationManagerWhenNoAlternativeIsAvailable() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ReactiveSecurityAutoConfiguration.class)
.doesNotHaveBean(EnableWebFluxSecurityConfiguration.class));
.hasBean("denyAllAuthenticationManager"));
}
@Test
void enablesWebFluxSecurityWhenUserDetailsServiceIsPresent() {
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class)
.run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull());
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(WebFilterChainProxy.class);
assertThat(context).doesNotHaveBean("denyAllAuthenticationManager");
});
}
@Test
void enablesWebFluxSecurityWhenReactiveAuthenticationManagerIsPresent() {
this.contextRunner
.withBean(ReactiveAuthenticationManager.class, () -> mock(ReactiveAuthenticationManager.class))
.run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull());
.run((context) -> {
assertThat(context).hasSingleBean(WebFilterChainProxy.class);
assertThat(context).doesNotHaveBean("denyAllAuthenticationManager");
});
}
@Test
void enablesWebFluxSecurityWhenSecurityWebFilterChainIsPresent() {
this.contextRunner.withBean(SecurityWebFilterChain.class, () -> mock(SecurityWebFilterChain.class))
.run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull());
.run((context) -> {
assertThat(context).hasSingleBean(WebFilterChainProxy.class);
assertThat(context).doesNotHaveBean("denyAllAuthenticationManager");
});
}
@Test

Loading…
Cancel
Save