Spring Reactive Security认证组件作用及403报错问题排查
Spring WebFlux Reactive Security 接口定位与403问题排查
一、两个核心接口的职责边界
两个接口虽然都返回Mono<Authentication>,但逻辑边界非常清晰,不要混写:
ServerAuthenticationConverter:请求到未认证凭证的转换器,唯一职责就是从HTTP请求(ServerWebExchange)中提取认证相关的原始信息,比如Authorization头、Cookie中的token、请求参数携带的密钥等,把这些原始值封装成未认证状态的Authentication对象返回即可。这个阶段绝对不要做任何凭证校验、验签、查库的逻辑,没有合法认证参数的时候直接返回Mono.empty()就行。ReactiveAuthenticationManager:凭证校验器,入参就是Converter输出的未认证Authentication对象,唯一职责就是校验凭证合法性:比如JWT验签、账号密码比对、token有效性校验。校验通过后,必须返回已认证状态的Authentication对象,填充好用户标识、权限列表等信息;校验失败返回包装了认证异常的Mono.error(),无法判断认证结果就返回Mono.empty()。
简单记:Converter负责“把前端传的认证凭证捞出来装成对象”,Manager负责“校验这个凭证是不是真的、合法的,给合法凭证发认证通过的标识”。
二、403 Access Denied 问题根因
你现在拿到403是三个问题叠加导致的:
ReactiveAuthenticationManager实现完全为空,直接把入参的未认证Authentication原样返回,这个对象的isAuthenticated()始终为false,Spring Security根本不认为当前请求是已登录状态。- 自定义Converter逻辑有缺陷:直接取Authorization头的List值没有判空,没有处理Bearer前缀,没有过滤非Bearer类型的认证头,很容易出现空指针。
- 配置中禁用了匿名访问,且过滤器位置配置错误,当请求没有合法认证信息时,不会返回401未认证,直接走到权限校验逻辑返回403。
三、修复方案
1. 修正Converter逻辑
去掉冗余的链式调用,增加空判断和Bearer前缀处理:
@Component public class GitJwtServerAuthenticationConverter implements ServerAuthenticationConverter { private static final String BEARER_PREFIX = "Bearer "; @Override public Mono<Authentication> convert(ServerWebExchange exchange) { return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION)) .filter(headerValue -> headerValue.startsWith(BEARER_PREFIX)) .map(headerValue -> headerValue.substring(BEARER_PREFIX.length())) .map(rawToken -> new GitBearerTokenAuthenticationToken(rawToken)); } }
这个实现不会在没有合法Authorization头的时候抛异常,会直接返回空Mono交给后续流程处理。
2. 补全AuthenticationManager的校验逻辑
在Manager中完成JWT验签、解析,构造已认证的Authentication对象返回:
public class GitJwtReactiveAuthenticationManager implements ReactiveAuthenticationManager { // 注入你自己的JWT解析工具 private final JwtParser jwtParser; public GitJwtReactiveAuthenticationManager(JwtParser jwtParser) { this.jwtParser = jwtParser; } @Override public Mono<Authentication> authenticate(Authentication authentication) { String rawToken = (String) authentication.getCredentials(); try { Jws<Claims> claimsJws = jwtParser.parseClaimsJws(rawToken); Claims claims = claimsJws.getBody(); String userId = claims.getSubject(); // 从JWT claims中提取权限,组装成GrantedAuthority列表 List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); // 调用三参构造方法生成已认证的Token对象,此时isAuthenticated()为true GitBearerTokenAuthenticationToken authenticatedToken = new GitBearerTokenAuthenticationToken(userId, rawToken, authorities); return Mono.just(authenticatedToken); } catch (JwtException e) { // 验签失败、token过期等场景直接返回认证异常 return Mono.error(new BadCredentialsException("无效的访问凭证")); } } }
注意你的GitBearerTokenAuthenticationToken需要新增三参数构造方法,调用父类AbstractAuthenticationToken的三参构造,会自动将认证状态设为true。
3. 修正安全配置
调整过滤器注册位置,恢复匿名访问配置,增加异常处理逻辑:
@Configuration @EnableWebFluxSecurity public class SecurityConfiguration { @Bean ReactiveAuthenticationManager reactiveAuthenticationManager(JwtParser jwtParser) { return new GitJwtReactiveAuthenticationManager(jwtParser); } @Bean SecurityWebFilterChain springSecurityFilterChain( ServerHttpSecurity http, ReactiveAuthenticationManager reactiveAuthenticationManager, ServerAuthenticationConverter serverAuthenticationConverter ) { AuthenticationWebFilter jwtAuthFilter = new AuthenticationWebFilter(reactiveAuthenticationManager); jwtAuthFilter.setServerAuthenticationConverter(serverAuthenticationConverter); return http .httpBasic(HttpBasicSpec::disable) .csrf(CsrfSpec::disable) .formLogin(FormLoginSpec::disable) .logout(LogoutSpec::disable) .authorizeExchange(authorize -> authorize .pathMatchers("/actuator/**", "/login/**").permitAll() .anyExchange().authenticated() ) // 把JWT认证过滤器加在授权过滤器之前即可,不要直接替换AUTHENTICATION位置的原有过滤器 .addFilterBefore(jwtAuthFilter, SecurityWebFiltersOrder.AUTHORIZATION) .exceptionHandling(ex -> ex // 未认证请求返回401 .authenticationEntryPoint((exchange, e) -> { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().writeWith( Mono.just(exchange.getResponse().bufferFactory().wrap("未认证".getBytes(StandardCharsets.UTF_8))) ); }) // 已认证但无权限返回403 .accessDeniedHandler((exchange, e) -> { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().writeWith( Mono.just(exchange.getResponse().bufferFactory().wrap("无访问权限".getBytes(StandardCharsets.UTF_8))) ); }) ) .build(); } }
改完之后重新启动服务,携带合法JWT的请求就能正常通过认证,不会再返回403。
内容的提问来源于stack exchange,提问作者Jordi
相关产品推荐
相关产品推荐

