6.1 响应式安全

Spring WebFlux 响应式安全体系:SecurityWebFilterChain 配置、JWT 鉴权与 ReactorContext 传递、ReactiveAuthenticationManager、密码编码、无状态会话与 public/private 路径声明。

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 SecurityWebFlux Security
配置类SecurityFilterChainSecurityWebFilterChain
配置入口HttpSecurityServerHttpSecurity
认证管理器AuthenticationManagerReactiveAuthenticationManager
用户详情UserDetailsServiceReactiveUserDetailsService
安全上下文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 + SecurityWebFilterChainServerHttpSecurity 不是 HttpSecurity
用户服务ReactiveUserDetailsService返回 Mono<UserDetails>
认证管理器ReactiveAuthenticationManager异步认证
JWTServerAuthenticationConverter + ReactiveJwtDecoderconverter 只提取凭证,校验交给 manager
安全上下文Reactor Context不是 ThreadLocal
方法级安全@PreAuthorize + @EnableReactiveMethodSecuritySpEL 延迟求值
密码编码BCryptPasswordEncoder强度 ≥ 10

下一节 6.2 CORS、CSRF 与会话 :跨域配置、无状态 API 的 CSRF 策略、分布式会话替代方案。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南