WebFlux 安全
Spring Security 的 WebFlux 支持依赖于一个 WebFilter,并且对于 Spring WebFlux 和 Spring WebFlux.Fn 都同样有效。以下是一些演示代码的示例应用程序
-
Hello WebFlux hellowebflux
-
Hello WebFlux.Fn hellowebfluxfn
-
Hello WebFlux 方法 hellowebflux-method
最小 WebFlux 安全配置
以下列表显示了一个最小的 WebFlux 安全配置
-
Java
-
Kotlin
@Configuration
@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
}
@Configuration
@EnableWebFluxSecurity
class HelloWebfluxSecurityConfig {
@Bean
fun userDetailsService(): ReactiveUserDetailsService {
val userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build()
return MapReactiveUserDetailsService(userDetails)
}
}
此配置提供表单和 HTTP 基本认证,设置授权以要求认证用户访问任何页面,设置默认登录页面和默认注销页面,设置安全相关的 HTTP 头部,添加 CSRF 保护等等。
显式 WebFlux 安全配置
以下页面显示了最小 WebFlux 安全配置的显式版本
-
Java
-
Kotlin
@Configuration
@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.httpBasic(withDefaults())
.formLogin(withDefaults());
return http.build();
}
}
import org.springframework.security.config.web.server.invoke
@Configuration
@EnableWebFluxSecurity
class HelloWebfluxSecurityConfig {
@Bean
fun userDetailsService(): ReactiveUserDetailsService {
val userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build()
return MapReactiveUserDetailsService(userDetails)
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin { }
httpBasic { }
}
}
}
请确保导入 org.springframework.security.config.web.server.invoke 函数以在您的类中启用 Kotlin DSL,因为 IDE 不总是会自动导入该方法,这会导致编译问题。 |
此配置显式设置了与最小配置相同的所有内容。从这里,您可以更容易地更改默认值。
您可以通过在 config/src/test/ 目录中搜索 EnableWebFluxSecurity 来找到更多显式配置的示例。
多链支持
您可以配置多个 SecurityWebFilterChain 实例,通过 RequestMatcher 实例来分离配置。
例如,您可以为以 /api 开头的 URL 隔离配置
-
Java
-
Kotlin
@Configuration
@EnableWebFluxSecurity
static class MultiSecurityHttpConfig {
@Order(Ordered.HIGHEST_PRECEDENCE) (1)
@Bean
SecurityWebFilterChain apiHttpSecurity(ServerHttpSecurity http) {
http
.securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/**")) (2)
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt); (3)
return http.build();
}
@Bean
SecurityWebFilterChain webHttpSecurity(ServerHttpSecurity http) { (4)
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.httpBasic(withDefaults()); (5)
return http.build();
}
@Bean
ReactiveUserDetailsService userDetailsService() {
return new MapReactiveUserDetailsService(
PasswordEncodedUser.user(), PasswordEncodedUser.admin());
}
}
import org.springframework.security.config.web.server.invoke
@Configuration
@EnableWebFluxSecurity
open class MultiSecurityHttpConfig {
@Order(Ordered.HIGHEST_PRECEDENCE) (1)
@Bean
open fun apiHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**")) (2)
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { } (3)
}
}
}
@Bean
open fun webHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { (4)
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
httpBasic { } (5)
}
}
@Bean
open fun userDetailsService(): ReactiveUserDetailsService {
return MapReactiveUserDetailsService(
PasswordEncodedUser.user(), PasswordEncodedUser.admin()
)
}
}
| 1 | 配置一个带有 @Order 的 SecurityWebFilterChain,以指定 Spring Security 应该首先考虑哪个 SecurityWebFilterChain |
| 2 | 使用 PathPatternParserServerWebExchangeMatcher 来声明此 SecurityWebFilterChain 将仅适用于以 /api/ 开头的 URL 路径 |
| 3 | 指定将用于 /api/** 端点的认证机制 |
| 4 | 创建另一个优先级较低的 SecurityWebFilterChain 实例以匹配所有其他 URL |
| 5 | 指定将用于应用程序其余部分的认证机制 |
Spring Security 为每个请求选择一个 SecurityWebFilterChain @Bean。它按照 securityMatcher 的定义顺序匹配请求。
在这种情况下,这意味着如果 URL 路径以 /api 开头,Spring Security 使用 apiHttpSecurity。如果 URL 不以 /api 开头,Spring Security 默认使用 webHttpSecurity,它有一个隐式的 securityMatcher,匹配任何请求。
模块化 ServerHttpSecurity 配置
许多用户更喜欢将他们的 Spring Security 配置集中在一个地方,并选择在 SecurityWebFilterChain Bean 声明中进行配置。然而,有时用户可能希望将配置模块化。这可以通过以下方式完成
Customizer<ServerHttpSecurity> Bean
如果您想模块化您的安全配置,可以将逻辑放在 Customizer<ServerHttpSecurity> Bean 中。例如,以下配置将确保所有 ServerHttpSecurity 实例都配置为
-
Java
-
Kotlin
@Bean
Customizer<ServerHttpSecurity> httpSecurityCustomizer() {
return (http) -> http
.headers((headers) -> headers
.contentSecurityPolicy((csp) -> csp
(1)
.policyDirectives("object-src 'none'")
)
)
(2)
.redirectToHttps(Customizer.withDefaults());
}
@Bean
fun httpSecurityCustomizer(): Customizer<ServerHttpSecurity> {
return Customizer { http -> http
.headers { headers -> headers
.contentSecurityPolicy { csp -> csp
(1)
.policyDirectives("object-src 'none'")
}
}
(2)
.redirectToHttps(Customizer.withDefaults())
}
}
| 1 | 将 内容安全策略 设置为 object-src 'none' |
| 2 | 将任何请求重定向到 https |
顶级 ServerHttpSecurity Customizer Bean
如果您更喜欢进一步模块化您的安全配置,Spring Security 将自动应用任何顶级 HttpSecurity Customizer Bean。
顶级 HttpSecurity Customizer 类型可以概括为任何与 public HttpSecurity.*(Customizer<T>) 匹配的 Customizer<T>。这表示任何作为 HttpSecurity 上公共方法的单个参数的 Customizer<T>。
一些示例可以帮助澄清。如果 Customizer<ContentTypeOptionsConfig> 作为 Bean 发布,它将不会自动应用,因为它是一个参数 HeadersConfigurer.contentTypeOptions(Customizer),而这不是在 HttpSecurity 上定义的方法。然而,如果 Customizer<HeadersConfigurer<HttpSecurity>> 作为 Bean 发布,它将自动应用,因为它是一个参数 HttpSecurity.headers(Customizer)。
例如,以下配置将确保 内容安全策略 设置为 object-src 'none'
-
Java
-
Kotlin
@Bean
Customizer<ServerHttpSecurity.HeaderSpec> headersSecurity() {
return (headers) -> headers
.contentSecurityPolicy((csp) -> csp
(1)
.policyDirectives("object-src 'none'")
);
}
@Bean
fun headersSecurity(): Customizer<ServerHttpSecurity.HeaderSpec> {
return Customizer { headers -> headers
.contentSecurityPolicy { csp -> csp
(1)
.policyDirectives("object-src 'none'")
}
}
}
Customizer Bean 排序
首先,每个 Customizer<HttpSecurity> Bean 都使用 ObjectProvider#orderedStream() 应用。这意味着如果存在多个 Customizer<HttpSecurity> Bean,可以在 Bean 定义中添加 @Order 注解来控制排序。
接下来,查找每个 顶级 HttpSecurity Customizer Bean 类型,并使用 ObjectProvider#orderedStream() 应用每个类型。如果存在两个 Customizer<HeadersConfigurer<HttpSecurity>> Bean 和两个 Customizer<HttpsRedirectConfigurer<HttpSecurity>> 实例,则调用每个 Customizer 类型的顺序是未定义的。但是,每个 Customizer<HttpsRedirectConfigurer<HttpSecurity>> 实例的顺序由 ObjectProvider#orderedStream() 定义,并且可以使用 Bean 定义上的 @Order 进行控制。
最后,HttpSecurity Bean 作为 Bean 注入。所有 Customizer 实例都在 HttpSecurity Bean 创建之前应用。这允许覆盖 Customizer Bean 提供的自定义。
您可以在下面找到一个说明排序的示例
-
Java
-
Kotlin
@Bean (4)
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
http
.authorizeExchange((exchange) -> exchange
.anyExchange().authenticated()
);
return http.build();
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE) (2)
Customizer<ServerHttpSecurity> userAuthorization() {
return (http) -> http
.authorizeExchange((exchange) -> exchange
.pathMatchers("/users/**").hasRole("USER")
);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) (1)
Customizer<ServerHttpSecurity> adminAuthorization() {
return (http) -> http
.authorizeExchange((exchange) -> exchange
.pathMatchers("/admins/**").hasRole("ADMIN")
);
}
(3)
@Bean
Customizer<ServerHttpSecurity.HeaderSpec> contentSecurityPolicy() {
return (headers) -> headers
.contentSecurityPolicy((csp) -> csp
.policyDirectives("object-src 'none'")
);
}
@Bean
Customizer<ServerHttpSecurity.HeaderSpec> contentTypeOptions() {
return (headers) -> headers
.contentTypeOptions(Customizer.withDefaults());
}
@Bean
Customizer<ServerHttpSecurity.HttpsRedirectSpec> httpsRedirect() {
return Customizer.withDefaults();
}
@Bean (4)
fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
http
.authorizeExchange({ exchanges -> exchanges
.anyExchange().authenticated()
})
return http.build()
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE) (2)
fun userAuthorization(): Customizer<ServerHttpSecurity> {
return Customizer { http -> http
.authorizeExchange { exchanges -> exchanges
.pathMatchers("/users/**").hasRole("USER")
}
}
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) (1)
fun adminAuthorization(): Customizer<ServerHttpSecurity> {
return ThrowingCustomizer { http -> http
.authorizeExchange { exchanges -> exchanges
.pathMatchers("/admins/**").hasRole("ADMIN")
}
}
}
(3)
@Bean
fun contentSecurityPolicy(): Customizer<ServerHttpSecurity.HeaderSpec> {
return Customizer { headers -> headers
.contentSecurityPolicy { csp -> csp
.policyDirectives("object-src 'none'")
}
}
}
@Bean
fun contentTypeOptions(): Customizer<ServerHttpSecurity.HeaderSpec> {
return Customizer { headers -> headers
.contentTypeOptions(Customizer.withDefaults())
}
}
@Bean
fun httpsRedirect(): Customizer<ServerHttpSecurity.HttpsRedirectSpec> {
return Customizer.withDefaults()
}
| 1 | 首先应用所有 Customizer<HttpSecurity> 实例。adminAuthorization Bean 具有最高的 @Order,因此它首先应用。如果 Customizer<HttpSecurity> Bean 上没有 @Order 注解或 @Order 注解具有相同的值,则 Customizer<HttpSecurity> 实例的应用顺序是未定义的。 |
| 2 | userAuthorization 接下来应用,因为它是一个 Customizer<HttpSecurity> 实例 |
| 3 | Customizer 类型的顺序是未定义的。在此示例中,contentSecurityPolicy、contentTypeOptions 和 httpsRedirect 的顺序是未定义的。如果将 @Order(Ordered.HIGHEST_PRECEDENCE) 添加到 contentTypeOptions,那么我们将知道 contentTypeOptions 在 contentSecurityPolicy 之前(它们是相同的类型),但我们不知道 httpsRedirect 是在 Customizer<HeadersConfigurer<HttpSecurity>> Bean 之前还是之后。 |
| 4 | 应用所有 Customizer Bean 后,将 HttpSecurity 作为 Bean 传入。 |