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。
疑问:
- 为什么下游返回成功后,网关会触发AuthenticationEntryPoint?
- 如何让网关返回下游的正确响应?
- 下游返回错误时,如何通过
@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

