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())获取完整用户数据。
- 认证失败返回401:检查
JwtTokenProvider的validateToken方法逻辑,确保Token有效期、签名验证正确;同时确认userRepository.findByEmail能正确查询到用户。 - SecurityContext为空:确保Filter或SecurityContextRepository的执行顺序正确,必须在
SecurityWebFiltersOrder.AUTHENTICATION阶段处理认证,且不能跳过Spring Security的默认上下文处理流程。 - 用户信息为空:确认创建
UsernamePasswordAuthenticationToken时传入的principal不为空,建议使用完整User对象而非仅用户名。
内容的提问来源于stack exchange,提问作者I Want Answers

