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

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是三个问题叠加导致的:

  1. ReactiveAuthenticationManager实现完全为空,直接把入参的未认证Authentication原样返回,这个对象的isAuthenticated()始终为false,Spring Security根本不认为当前请求是已登录状态。
  2. 自定义Converter逻辑有缺陷:直接取Authorization头的List值没有判空,没有处理Bearer前缀,没有过滤非Bearer类型的认证头,很容易出现空指针。
  3. 配置中禁用了匿名访问,且过滤器位置配置错误,当请求没有合法认证信息时,不会返回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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 10:39:20