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

Spring Boot网关调用下游服务返回201却触发401认证异常

问题描述

我基于Spring Boot 3搭建了网关服务,引入了spring-boot-starter-web和spring-boot-starter-webflux(主要为结合HttpExchange使用WebClient)。同时单独搭建了一个Spring Boot下游服务,提供REST接口并操作数据库。

网关服务配置了Spring Security实现JWT认证授权,还通过@RestControllerAdvice统一处理错误响应。当前流程为:网关直接连接数据库完成用户认证授权(后续将切换为SSO OAuth2),认证通过后调用下游服务完成客户添加等业务操作。

异常现象:创建客户的API已通过网关的认证授权,请求成功到达下游服务并返回201状态码,但网关却返回401状态码,触发了自定义的AuthenticationEntryPoint响应。

已验证信息:

  • 测试控制器的认证逻辑正常:无效JWT返回401,无权限返回403;
  • 启用WebClient的错误过滤器后,日志明确显示下游返回201,但网关仍返回401。

疑问:

  1. 为什么下游返回成功后,网关会触发AuthenticationEntryPoint?
  2. 如何让网关返回下游的正确响应?
  3. 下游返回错误时,如何通过@RestControllerAdvice或WebClient过滤器处理?

问题分析与解决方案

1. 下游返回成功却触发AuthenticationEntryPoint的原因

核心原因是WebClient调用下游服务时,请求上下文的SecurityContext被意外清空或丢失。

Spring Security的认证信息存储在SecurityContextHolder中,而WebFlux的异步线程模型下,如果WebClient调用未正确传递SecurityContext,或调用过程中(如错误处理、线程切换)导致SecurityContext被重置,网关处理下游响应准备返回时,SecurityContextHolder中无有效认证信息,Spring Security会判定当前请求未认证,进而触发AuthenticationEntryPoint。

另外一种小概率情况是:网关的Spring Security配置误将下游响应处理的路径纳入认证拦截范围,但结合你已验证测试控制器认证逻辑正常的情况,这种可能性较低。

2. 如何返回正确响应

方案一:确保WebClient调用传递SecurityContext

在Spring Boot 3中,可通过ExchangeFilterFunction手动传递SecurityContext,示例代码如下:

@Bean
public WebClient webClient() {
    return WebClient.builder()
            .filter((request, next) -> {
                return SecurityContextHolder.getContext()
                        .map(context -> {
                            // 从SecurityContext中取出JWT,传递到下游请求头
                            JwtAuthenticationToken authToken = (JwtAuthenticationToken) context.getAuthentication();
                            String jwt = authToken.getToken().getTokenValue();
                            return ClientRequest.from(request)
                                    .header("Authorization", "Bearer " + jwt)
                                    .build();
                        })
                        .defaultIfEmpty(request)
                        .flatMap(next::exchange);
            })
            .build();
}

方案二:调整Security配置,排除内部处理路径

检查Spring Security配置类,确保网关处理下游响应的控制器路径未被错误加入认证拦截规则:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/customers/create").authenticated()
                        // 排除网关内部处理下游响应的路径(如有)
                        .requestMatchers("/internal/downstream/**").permitAll()
                        .anyRequest().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
                .exceptionHandling(ex -> ex
                        .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                        .accessDeniedHandler(new CustomAccessDeniedHandler()));
        return http.build();
    }
}

方案三:处理响应时恢复SecurityContext

若WebClient调用过程中SecurityContext丢失,可在处理下游响应前手动恢复:

// 调用下游前保存当前SecurityContext
SecurityContext context = SecurityContextHolder.getContext();
webClient.post()
        .uri(downstreamCreateCustomerUrl)
        .bodyValue(customerRequest)
        .retrieve()
        .bodyToMono(Customer.class)
        .doOnSuccess(customer -> {
            // 恢复SecurityContext,避免网关返回时触发认证拦截
            SecurityContextHolder.setContext(context);
        })
        .subscribe();

3. 下游返回错误的处理方式

方式一:WebClient过滤器统一捕获错误

自定义ExchangeFilterFunction捕获下游错误响应,转换为自定义异常后交给全局异常处理器:

@Bean
public ExchangeFilterFunction downstreamErrorFilter() {
    return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
        if (clientResponse.statusCode().isError()) {
            return clientResponse.bodyToMono(String.class)
                    .flatMap(errorBody -> {
                        HttpStatus status = clientResponse.statusCode();
                        if (status == HttpStatus.BAD_REQUEST) {
                            return Mono.error(new DownstreamBadRequestException(errorBody));
                        } else if (status == HttpStatus.NOT_FOUND) {
                            return Mono.error(new DownstreamResourceNotFoundException(errorBody));
                        } else {
                            return Mono.error(new DownserverServiceException("下游服务异常: " + errorBody));
                        }
                    });
        }
        return Mono.just(clientResponse);
    });
}

// 注册过滤器到WebClient
@Bean
public WebClient webClient() {
    return WebClient.builder()
            .filter(downstreamErrorFilter())
            .build();
}

方式二:@RestControllerAdvice捕获异常

在网关的全局异常处理器中捕获WebClient抛出的异常,返回统一格式的错误响应:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(WebClientResponseException.class)
    public ResponseEntity<ErrorResponse> handleWebClientResponse(WebClientResponseException ex) {
        ErrorResponse response = new ErrorResponse(
                ex.getStatusCode().value(),
                ex.getStatusText(),
                ex.getResponseBodyAsString()
        );
        return new ResponseEntity<>(response, ex.getStatusCode());
    }

    @ExceptionHandler(DownstreamBadRequestException.class)
    public ResponseEntity<ErrorResponse> handleDownstreamBadRequest(DownstreamBadRequestException ex) {
        ErrorResponse response = new ErrorResponse(
                HttpStatus.BAD_REQUEST.value(),
                "下游参数错误",
                ex.getMessage()
        );
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    // 其他自定义异常处理逻辑...
}

// 自定义异常类示例
class DownstreamBadRequestException extends RuntimeException {
    public DownstreamBadRequestException(String message) {
        super(message);
    }
}

// 错误响应DTO
class ErrorResponse {
    private int code;
    private String msg;
    private String detail;

    // 构造函数、getter/setter
}

注意事项

  • 若网关基于WebFlux异步模型,建议优先使用Mono/Flux的onErrorResume等异步错误处理机制,避免使用block()导致线程阻塞和SecurityContext丢失;
  • 异步场景下@RestControllerAdvice可能无法覆盖所有异常,可结合WebExceptionHandler实现更全面的异步异常处理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 18:15:59