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

Spring Webflux集成Spring Security权限验证异常,疑上下文设置问题

Spring Webflux 集成 Spring Security 权限验证异常问题

环境版本

  • spring-boot-starter-webflux 3.3.4
  • spring-security-web 6.3.3
  • Java 21

问题场景

我在验证权限逻辑时,刻意忽略认证过滤器,通过Token访问接口并缩减用户权限,预期触发权限拒绝异常,但实际抛出AuthenticationCredentialsNotFoundException,提示未认证。

相关代码

JwtValidationWebFilter 实现

@Component
public class JwtValidationWebFilter implements WebFilter {

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String token = extractToken(exchange.getRequest());

        if (!StringUtils.hasText(token)) {
            return chain.filter(exchange);
        }

        LogUtils.info("从请求头中提取 token: {}", token);
        boolean validated = jwtTokenProvider.validateToken(token);
        if (!validated) {
            LogUtils.info("验证 token 失败");
            return chain.filter(exchange);
        }
        Authentication authentication = jwtTokenProvider.getAuthentication(token);
        try {
            LogUtils.info("提取的权限节点: {}", objectMapper.writeValueAsString(authentication.getAuthorities()));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        // 设置上下文
        Context context = ReactiveSecurityContextHolder.withAuthentication(authentication);
        return chain.filter(exchange).contextWrite(context);
    }

    private String extractToken(ServerHttpRequest request) {
        String bearerToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

SecurityConfig 配置

public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                        ServerAuthenticationEntryPoint serverAuthenticationEntryPoint,
                                                        ServerAccessDeniedHandler serverAccessDeniedHandler,
                                                        ReactiveAuthenticationManager reactiveAuthenticationManager,
                                                        ServerAuthenticationConverter serverAuthenticationConverter,
                                                        ServerAuthenticationSuccessHandler successHandler,
                                                        ServerAuthenticationFailureHandler failureHandler,
                                                        JwtValidationWebFilter jwtValidationWebFilter) {
    AuthenticationWebFilter authenticationWebFilter =
            authenticationWebFilter(
                    reactiveAuthenticationManager,
                    serverAuthenticationConverter,
                    successHandler,
                    failureHandler);

    SecurityWebFilterChain chain = http
            .csrf(ServerHttpSecurity.CsrfSpec::disable)
            .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
            .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
            .authenticationManager(reactiveAuthenticationManager)
            .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
            .logout(ServerHttpSecurity.LogoutSpec::disable)
            .authorizeExchange(exchanges -> exchanges
                    .pathMatchers("/api/auth/**").permitAll()
                    .pathMatchers("/api/sys-user/**").hasAnyAuthority("PERM_ABC")
                    .anyExchange().authenticated()
            )
            .addFilterBefore(jwtValidationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
            .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
            .exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec
                    .accessDeniedHandler(serverAccessDeniedHandler)
                    .authenticationEntryPoint(serverAuthenticationEntryPoint)
            )
            .build();
    return chain;
}

异常信息

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: Not Authenticated
    at org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter.commenceAuthentication(ExceptionTranslationWebFilter.java:96) ~[spring-security-web-6.3.3.jar:6.3.3]
    Suppressed: org.springframework.security.access.AccessDeniedException: Access Denied
        at org.springframework.security.authorization.ReactiveAuthorizationManager.lambda$verify$0(ReactiveAuthorizationManager.java:53) ~[spring-security-core-6.3.3.jar:6.3.3]
        Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.MonoError] :
    reactor.core.publisher.Mono.error(Mono.java:299)
    org.springframework.security.authorization.ReactiveAuthorizationManager.lambda$verify$0(ReactiveAuthorizationManager.java:53)

Caused by: org.springframework.security.authentication.InsufficientAuthenticationException: Full authentication is required to access this resource
.....

核心问题点

定位到ExceptionTranslationWebFilter的逻辑:

chain.filter(exchange)
        .onErrorResume(AccessDeniedException.class, (denied) -> exchange.getPrincipal()
            .filter((principal) -> (!(principal instanceof Authentication) || (principal instanceof Authentication
                    && (this.authenticationTrustResolver.isAuthenticated((Authentication) principal)))))
            .switchIfEmpty(commenceAuthentication(exchange,
                    new InsufficientAuthenticationException(
                            "Full authentication is required to access this resource")))
            .flatMap((principal) -> this.accessDeniedHandler.handle(exchange, denied))
            .then());

预期AccessDeniedException会进入flatMap的权限拒绝处理,但实际进入了switchIfEmpty分支,抛出未认证异常,怀疑上下文设置存在问题。


问题分析与解决方案

根源

  1. SecurityContextRepository 配置错误:使用NoOpServerSecurityContextRepository不会保存SecurityContext,后续权限过滤器无法从上下文获取已设置的Authentication。
  2. 冗余的AuthenticationWebFilter:同时添加了自定义Jwt过滤器和默认认证过滤器,后者可能覆盖或清除你设置的上下文。
  3. Authentication 对象状态:如果authentication.isAuthenticated()返回false,AuthenticationTrustResolver会判定为未认证,触发未认证分支。

修复步骤

  1. 替换SecurityContextRepository:用WebSessionServerSecurityContextRepository替代NoOpServerSecurityContextRepository,确保上下文能被后续过滤器获取:
    .securityContextRepository(new WebSessionServerSecurityContextRepository())
    
  2. 移除冗余的AuthenticationWebFilter:自定义Jwt过滤器已完成认证上下文设置,不需要重复的认证过滤器:
    // 移除以下两行
    // AuthenticationWebFilter authenticationWebFilter = authenticationWebFilter(...);
    // .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
    
  3. 确保Authentication已认证:创建Authentication对象时设置authenticated = true,例如使用UsernamePasswordAuthenticationToken的三参数构造:
    Authentication authentication = new UsernamePasswordAuthenticationToken(
            userDetails, null, userDetails.getAuthorities());
    

修改后的SecurityConfig关键片段:

SecurityWebFilterChain chain = http
        .csrf(ServerHttpSecurity.CsrfSpec::disable)
        .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
        .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
        .securityContextRepository(new WebSessionServerSecurityContextRepository())
        .logout(ServerHttpSecurity.LogoutSpec::disable)
        .authorizeExchange(exchanges -> exchanges
                .pathMatchers("/api/auth/**").permitAll()
                .pathMatchers("/api/sys-user/**").hasAnyAuthority("PERM_ABC")
                .anyExchange().authenticated()
        )
        .addFilterBefore(jwtValidationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
        .exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec
                .accessDeniedHandler(serverAccessDeniedHandler)
                .authenticationEntryPoint(serverAuthenticationEntryPoint)
        )
        .build();

内容的提问来源于stack exchange,提问作者L Jackie

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 11:21:03