WebFlux Security 配置
Spring Security 对 WebFlux 的适配基于 SecurityWebFilterChain:
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.web.server.SecurityWebFilterChain;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf(csrf -> csrf.disable()) // 无状态 API,禁用 CSRF
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/health", "/login").permitAll()
.pathMatchers("/api/admin/**").hasRole("ADMIN")
.pathMatchers("/api/**").authenticated()
.anyExchange().denyAll()
)
.httpBasic(httpBasic -> httpBasic.disable()) // 不用 Basic Auth
.formLogin(formLogin -> formLogin.disable()) // 不用表单登录
.build();
}
}
与 Spring MVC Security 的关键区别:
| 维度 | MVC Security | WebFlux Security |
|---|---|---|
| 配置类 | SecurityFilterChain | SecurityWebFilterChain |
| 配置入口 | HttpSecurity | ServerHttpSecurity |
| 认证管理器 | AuthenticationManager | ReactiveAuthenticationManager |
| 用户详情 | UserDetailsService | ReactiveUserDetailsService |
| 安全上下文 | SecurityContextHolder(ThreadLocal) | ReactorContext |
ReactiveUserDetailsService
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import reactor.core.publisher.Mono;
@Service
public class EventdUserDetailsService implements ReactiveUserDetailsService {
private final Map<String, UserDetails> users = Map.of(
"alice", User.withUsername("alice")
.password("{noop}password") // {noop} 表示不编码,生产用 bcrypt
.roles("USER")
.build(),
"admin", User.withUsername("admin")
.password("{bcrypt}$2a$10$...")
.roles("ADMIN", "USER")
.build()
);
@Override
public Mono<UserDetails> findByUsername(String username) {
return Mono.justOrEmpty(users.get(username));
}
}
{bcrypt} 前缀只有配合 DelegatingPasswordEncoder 才会被识别:它按前缀选择对应的编码器。如果应用只注册了一个 BCryptPasswordEncoder,密码字符串里不要带 {bcrypt} 前缀,否则编码器会把前缀当成密码内容。不要在代码中硬编码密码——真实的用户数据来自数据库或 LDAP。
JWT 鉴权
无状态 API 的典型方案是 JWT。自定义 ServerAuthenticationConverter 从请求头提取 token:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class JwtAuthenticationConverter implements ServerAuthenticationConverter {
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
String header = exchange.getRequest().getHeaders().getFirst("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
return Mono.empty(); // 无 token,交给下一个 converter 处理
}
String token = header.substring(7);
// 两参构造器生成的是「未认证」token:这里只提取凭证,不信任 token 内容
return Mono.just(new UsernamePasswordAuthenticationToken(token, token));
}
}
真正的签名、过期时间、issuer/audience 校验放在 ReactiveAuthenticationManager 里做。不要像很多示例那样在 converter 里直接构造带 authorities 的三参 token:那个构造器会把 authenticated 设为 true,后续 manager 再判断 isAuthenticated() 已经没有意义。
整合到 SecurityWebFilterChain
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import reactor.core.publisher.Mono;
import java.util.List;
public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private final ReactiveJwtDecoder jwtDecoder;
public JwtReactiveAuthenticationManager(ReactiveJwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
String token = (String) authentication.getCredentials();
// decode() 负责验签、校验 exp/nbf 和 issuer(配置 issuer-uri 时);
// audience 默认不校验,需要自己组合 OAuth2TokenValidator
return jwtDecoder.decode(token)
.map(jwt -> {
String role = jwt.getClaimAsString("role");
// 缺少 role 时不给任何权限,而不是退化成 ROLE_null 或默认 USER
List<SimpleGrantedAuthority> authorities = role == null || role.isBlank()
? List.of()
: List.of(new SimpleGrantedAuthority("ROLE_" + role));
return (Authentication) new UsernamePasswordAuthenticationToken(
jwt.getSubject(), null, authorities);
})
.onErrorMap(JwtException.class,
err -> new BadCredentialsException("invalid JWT", err));
}
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http,
JwtAuthenticationConverter jwtAuth,
JwtReactiveAuthenticationManager jwtManager) {
AuthenticationWebFilter jwtFilter = new AuthenticationWebFilter(jwtManager);
jwtFilter.setServerAuthenticationConverter(jwtAuth);
return http
.csrf(csrf -> csrf.disable())
.addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/health", "/login").permitAll()
.pathMatchers("/api/**").authenticated()
)
.build();
}
addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION) 把 JWT 过滤器插入到认证阶段。这一段要替换掉本节开头那个只做路径授权的 securityWebFilterChain:同一个应用里只能有一个同名的 @Bean,不要两段都留着。
ReactiveJwtDecoder 不需要自己写。引入 spring-boot-starter-oauth2-resource-server 后,Boot 会按下面的配置自动创建基于 JWK Set 的 Nimbus 解码器:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com/
# 或者不用 issuer-uri,直接给 jwk-set-uri 和 jws-algorithm
issuer-uri 会触发启动时拉取 OIDC 元数据(/.well-known/openid-configuration),并自动校验 iss 声明。需要按自定义 claim 校验 audience 时,用 OAuth2TokenValidator 组合 JwtIssuerValidator 与 JwtClaimValidator,再 NimbusReactiveJwtDecoder.withJwkSetUri(...).build() 后 setJwtValidator(...)。
另一个容易踩的坑:如果只想拿 token 校验而不启用完整的 resource-server 自动配置,别把 ReactiveJwtDecoder 注入写成 @Autowired(required = true) 却没有配置 spring.security.oauth2.resourceserver.jwt.*——那样启动直接失败。要么给全配置,要么手动提供 NimbusReactiveJwtDecoder Bean。
ReactorContext 中的安全上下文
WebFlux 的请求可能在多个线程上处理,不能用 ThreadLocal 存储认证信息。Spring Security 把 Authentication 放在 Reactor 的 Context 中:
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import reactor.util.context.Context;
@GetMapping("/api/me")
public Mono<Map<String, String>> me() {
return Mono.deferContextual(ctx -> {
Authentication auth = ctx.getOrDefault(
SecurityContext.class,
new SecurityContextImpl(null)
).getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
return Mono.error(new RuntimeException("Not authenticated"));
}
return Mono.just(Map.of(
"username", auth.getName(),
"authorities", auth.getAuthorities().toString()
));
});
}
Mono.deferContextual() 读取 Reactor Context,这是响应式链中传递「隐式上下文」的标准方式。Spring Security 在请求进入时把认证信息写入 Context,后续所有操作符都可以读取。
方法级安全
使用 @PreAuthorize 在方法层面控制权限:
import org.springframework.security.access.prepost.PreAuthorize;
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.name")
public Mono<Order> getOrder(Long orderId, String userId) {
// 只有 ADMIN 或订单所有者可以查看
return orderRepository.findById(orderId);
}
@PreAuthorize("hasRole('ADMIN')")
public Mono<Void> deleteOrder(Long orderId) {
return orderRepository.deleteById(orderId);
}
}
启用方法级安全:
@EnableReactiveMethodSecurity
@Configuration
public class MethodSecurityConfig {
}
注意:@PreAuthorize 在方法调用前求值,它本身不做异步查询;如果权限判断依赖数据库等异步数据,应改用自定义 ReactiveAuthorizationManager,而不是把异步调用塞进 SpEL。#userId 这类参数名解析还依赖编译时保留参数名(-parameters),否则要写成 @P("userId")。
密码编码器
永远不要明文存储密码。Spring Security 提供多种编码器:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength 12,迭代 2^12 次
}
| 编码器 | 特点 | 适用场景 |
|---|---|---|
BCryptPasswordEncoder | 自适应慢哈希,自动加盐 | 生产常用;OWASP 现在更优先推荐 Argon2id |
Pbkdf2PasswordEncoder | 标准 PBKDF2 | 合规要求 |
Argon2PasswordEncoder | 内存困难,抗 GPU | 高安全场景 |
DelegatingPasswordEncoder | 兼容多种编码格式 | 迁移旧系统 |
eventd v2 安全策略
eventd v2 的安全层配置:
/public/health → permitAll
/public/login → permitAll
/api/** → JWT 认证(ROLE_USER 或 ROLE_ADMIN)
/api/admin/** → ROLE_ADMIN
/ws/** → JWT 认证(WebSocket upgrade 时检查)
内部服务调用(gateway → api → data)走mTLS(双向 TLS 证书认证),不经过 JWT。这减少了内部服务间的 token 验证开销,且证书比 JWT 更难伪造。
小结
| 概念 | WebFlux 实现 | 注意 |
|---|---|---|
| 安全链 | @EnableWebFluxSecurity + SecurityWebFilterChain | ServerHttpSecurity 不是 HttpSecurity |
| 用户服务 | ReactiveUserDetailsService | 返回 Mono<UserDetails> |
| 认证管理器 | ReactiveAuthenticationManager | 异步认证 |
| JWT | ServerAuthenticationConverter + ReactiveJwtDecoder | converter 只提取凭证,校验交给 manager |
| 安全上下文 | Reactor Context | 不是 ThreadLocal |
| 方法级安全 | @PreAuthorize + @EnableReactiveMethodSecurity | SpEL 延迟求值 |
| 密码编码 | BCryptPasswordEncoder | 强度 ≥ 10 |
下一节 6.2 CORS、CSRF 与会话 :跨域配置、无状态 API 的 CSRF 策略、分布式会话替代方案。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。