You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring WebFlux API Token认证实现及安全上下文绑定问题求助

问题描述

尝试通过ReactiveAuthenticationManager和WebFilter两种方式实现Spring WebFlux认证,Token已成功解码且用户信息能正确加载,但无法将认证信息绑定到ReactiveSecurityContextHolder,后续从上下文读取时抛出NullPointerException,请求偶尔返回401,控制器无法获取用户信息。使用Spring Boot 3.3.4。

现有核心代码如下:

JwtReactiveAuthenticationManager实现

@Component
@RequiredArgsConstructor
public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager {

    private final JwtTokenProvider jwtTokenProvider;
    private final UserRepository userRepository;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String token = authentication.getCredentials().toString();

        if (!jwtTokenProvider.validateToken(token)) {
            System.out.println("Invalid token");
            return Mono.empty();
        }
        String username = jwtTokenProvider.getUsernameFromToken(token);
        return userRepository.findByEmail(username)
            .switchIfEmpty(Mono.error(new RuntimeException("User not found")))
            .map(user -> {
                System.out.println("hydrated user from token. File is JRAM");
                System.out.println(user); // 能正常打印用户信息
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        user.getUsername(),
                        null,
                        Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
                );

                return authenticationToken;
            });
    }
}

尝试过的上下文绑定代码

SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
Context context = ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext));

尝试过的ServerSecurityContextRepository实现

@Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
    String authHeader = swe.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);

    if (authHeader != null && authHeader.startsWith("Bearer ")) {
        String authToken = authHeader.substring(7);
        Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
        return authenticationManager.authenticate(auth).map(SecurityContextImpl::new);
    } else {
        return Mono.empty();
    }
}

当前SecurityConfig配置

http
        .csrf(ServerHttpSecurity.CsrfSpec::disable)
        .cors(httpSecurityCorsConfigurer -> httpSecurityCorsConfigurer.configurationSource(request -> {
            CorsConfiguration config = new CorsConfiguration();
            // 自定义CORS配置
            return config;
        }))
        /*.authenticationManager(authenticationManager)
        .securityContextRepository(securityContextRepository)*/
        .authorizeExchange(authorizeExchangeSpec -> {
            authorizeExchangeSpec.pathMatchers(
                    "api/v1/user/login",
                    "api/v1/user/register","webjars/**",
                    "/v3/api-docs/**", "/swagger-ui.html",
                    "/swagger-ui/**").permitAll();
            authorizeExchangeSpec.anyExchange().authenticated();
        })
        .addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
        .build();

解决方案

针对Spring Boot 3.3.4(对应Spring Security 6.3.x),提供两种可行的认证上下文绑定方案,二选一即可:

方案一:使用ReactiveAuthenticationManager + ServerSecurityContextRepository

1. 完善ServerSecurityContextRepository实现

确保save方法正确实现(Spring Security需要该方法来处理上下文生命周期),同时处理认证失败的情况:

@Component
@RequiredArgsConstructor
public class JwtSecurityContextRepository implements ServerSecurityContextRepository {

    private final JwtReactiveAuthenticationManager authenticationManager;

    @Override
    public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
        // WebFlux中默认不需要持久化上下文,直接返回empty即可
        return Mono.empty();
    }

    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) {
        String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return Mono.empty();
        }

        String token = authHeader.substring(7);
        // 创建未认证的Authentication对象,传入token作为凭证
        Authentication authRequest = new UsernamePasswordAuthenticationToken(null, token);

        return authenticationManager.authenticate(authRequest)
                .map(SecurityContextImpl::new)
                // 认证失败时返回empty,让Spring Security自动处理401响应
                .onErrorResume(e -> Mono.empty());
    }
}

2. 修正SecurityConfig配置

启用authenticationManager和securityContextRepository,移除自定义的JwtAuthenticationFilter(该方案无需额外Filter):

@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtReactiveAuthenticationManager authenticationManager;
    private final JwtSecurityContextRepository securityContextRepository;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .cors(cors -> cors.configurationSource(request -> {
                    CorsConfiguration config = new CorsConfiguration();
                    // 补充你的CORS配置,比如允许的Origin、方法等
                    config.addAllowedOrigin("*");
                    config.addAllowedMethod("*");
                    config.addAllowedHeader("*");
                    return config;
                }))
                // 绑定认证管理器和上下文仓库
                .authenticationManager(authenticationManager)
                .securityContextRepository(securityContextRepository)
                .authorizeExchange(exchanges -> exchanges
                        .pathMatchers(
                                "/api/v1/user/login",
                                "/api/v1/user/register",
                                "/webjars/**",
                                "/v3/api-docs/**",
                                "/swagger-ui.html",
                                "/swagger-ui/**"
                        ).permitAll()
                        .anyExchange().authenticated()
                );
        return http.build();
    }
}

方案二:使用自定义JWT WebFilter

如果坚持用WebFilter方式,需要在Filter中正确设置SecurityContext,并确保Filter顺序正确:

1. 实现JwtAuthenticationWebFilter

@Component
@RequiredArgsConstructor
public class JwtAuthenticationWebFilter extends AuthenticationWebFilter {

    public JwtAuthenticationWebFilter(JwtReactiveAuthenticationManager authenticationManager) {
        super(authenticationManager);
        // 设置认证转换器:从请求中提取Token并转换为Authentication对象
        setServerAuthenticationConverter(exchange -> {
            String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                return Mono.empty();
            }
            String token = authHeader.substring(7);
            return Mono.just(new UsernamePasswordAuthenticationToken(null, token));
        });
        // 设置认证成功处理器:将认证信息存入SecurityContext
        setAuthenticationSuccessHandler((webFilterExchange, authentication) -> {
            SecurityContext context = new SecurityContextImpl(authentication);
            return ReactiveSecurityContextHolder.withSecurityContext(Mono.just(context))
                    .then(webFilterExchange.getChain().filter(webFilterExchange.getExchange()));
        });
        // 设置认证失败处理器
        setAuthenticationFailureHandler((webFilterExchange, exception) -> {
            webFilterExchange.getExchange().getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return webFilterExchange.getChain().filter(webFilterExchange.getExchange());
        });
    }
}

2. 修正SecurityConfig配置

移除注释的authenticationManager和securityContextRepository,保留自定义Filter:

@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationWebFilter jwtAuthenticationWebFilter;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .cors(cors -> cors.configurationSource(request -> {
                    CorsConfiguration config = new CorsConfiguration();
                    // 补充你的CORS配置
                    config.addAllowedOrigin("*");
                    config.addAllowedMethod("*");
                    config.addAllowedHeader("*");
                    return config;
                }))
                .authorizeExchange(exchanges -> exchanges
                        .pathMatchers(
                                "/api/v1/user/login",
                                "/api/v1/user/register",
                                "/webjars/**",
                                "/v3/api-docs/**",
                                "/swagger-ui.html",
                                "/swagger-ui/**"
                        ).permitAll()
                        .anyExchange().authenticated()
                )
                // 将自定义Filter添加到认证阶段
                .addFilterAt(jwtAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        return http.build();
    }
}

控制器中获取认证信息的正确方式

在WebFlux控制器中,有两种便捷方式获取已认证用户信息:

方式一:直接注入Authentication

@RestController
@RequestMapping("/api/v1/test")
public class TestController {

    @GetMapping
    public Mono<String> getCurrentUser(Authentication authentication) {
        // authentication即为已认证的用户信息
        return Mono.just("当前用户:" + authentication.getName());
    }
}

方式二:通过ReactiveSecurityContextHolder获取

@RestController
@RequestMapping("/api/v1/test")
public class TestController {

    @GetMapping
    public Mono<String> getCurrentUser() {
        return ReactiveSecurityContextHolder.getContext()
                .map(SecurityContext::getAuthentication)
                .map(auth -> "当前用户:" + auth.getName());
    }
}

如果需要完整用户对象,可在JwtReactiveAuthenticationManager中创建UsernamePasswordAuthenticationToken时传入完整User实例:

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
        user, // 传入完整User对象
        null,
        Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
);

之后在控制器中可通过((User) authentication.getPrincipal())获取完整用户数据。


常见问题排查
  1. 认证失败返回401:检查JwtTokenProvider的validateToken方法逻辑,确保Token有效期、签名验证正确;同时确认userRepository.findByEmail能正确查询到用户。
  2. SecurityContext为空:确保Filter或SecurityContextRepository的执行顺序正确,必须在SecurityWebFiltersOrder.AUTHENTICATION阶段处理认证,且不能跳过Spring Security的默认上下文处理流程。
  3. 用户信息为空:确认创建UsernamePasswordAuthenticationToken时传入的principal不为空,建议使用完整User对象而非仅用户名。

内容的提问来源于stack exchange,提问作者I Want Answers

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 06:24:56