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

Spring Boot:WebClient抛出的异常无法被异常控制器捕获

问题:Gateway中GlobalFilter抛出的异常无法被@ControllerAdvice捕获

我正在实现一个GlobalFilter组件,调用名为“uaa”的微服务验证Token。但当Token无效时抛出AuthorizationForbiddenException,该异常无法被我的异常控制器捕获。

我的AuthFilter代码

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthFilter implements GlobalFilter {

    private final JwtTokenProviderService jwtTokenProviderService;
    private final TokenStatusDaoService tokenStatusDaoService;
    private final WebClient webClient;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("something in the way");
        List<String> headers = exchange.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION);
        if(CollectionUtils.isEmpty(headers)) {
            log.trace("Request came without token");
            return chain.filter(exchange);
        } else {
            String authToken = headers.get(0);
            log.trace("Request holds a token");
            log.debug("Check if token has expired ...");
            if(jwtTokenProviderService.isTokenExpired(authToken)) {
                log.debug("Token has expired will throw an error");
                throw new AuthorizationForbiddenException(AuthorizationForbiddenExceptionTitleEnum.TOKEN_HAS_EXPIRED, "Token has expired");

            }else {
                log.debug("Check if token is valid and already saved");
                String userId = jwtTokenProviderService.getClaimsFromToken(authToken).get(SecurityUtils.IDENTIFIER_KEY).toString();
                if(!tokenStatusDaoService.exists(TokenStatusSpecification.withToken(authToken).and(TokenStatusSpecification.withUserId(Long.parseLong(userId))))) {
                    return webClient.get()
                            .uri("http://uaa", uriBuilder -> uriBuilder
                                    .path("/validate-token")
                                    .queryParam("token", authToken).build()).retrieve()
                            .bodyToMono(TokenValidationGetResource.class)
                            .map(tokenValidationGetResource -> {
                                if (!tokenValidationGetResource.isValid()) {
                                    log.debug("token is not valid");
                                    throw new AuthorizationForbiddenException(AuthorizationForbiddenExceptionTitleEnum.TOKEN_NOT_VALID, "Token is not valid");
                                } else {
                                    log.debug("token is valid");
                                    TokenStatusEntity tokenStatusEntity;
                                    try {
                                        tokenStatusEntity = tokenStatusDaoService.findOne(TokenStatusSpecification.withUserId(Long.parseLong(userId)));
                                    } catch (Exception e) {
                                        log.debug("No token defined for user: {}. Will save a new one ...", userId);
                                        tokenStatusEntity = new TokenStatusEntity();
                                    }
                                    tokenStatusEntity.setToken(authToken);
                                    tokenStatusEntity.setUserId(Long.parseLong(userId));
                                    tokenStatusEntity.setStatus(TokenStatusEnum.VALID);
                                    tokenStatusDaoService.save(tokenStatusEntity);
                                    log.debug("Token status entity: {}", tokenStatusEntity);
                                    return exchange;
                                }
                            }).flatMap(chain::filter);
                } else {
                    log.debug("Token exists in DB");
                    return chain.filter(exchange);
                }
            }
        }
    }
}

我的异常控制器代码

@ControllerAdvice
public class ExceptionControllerImpl implements ExceptionController {

    @Override
    @ExceptionHandler({
            AuthorizationForbiddenException.class
    })
    public ResponseEntity<ErrorDetailResource> handleGenericExceptions(
            AbstractBaseException e, HttpServletRequest request) {
        ErrorDetailResource errorDetailResource = new ErrorDetailResource();
        errorDetailResource.setTimestamp(Instant.now().toEpochMilli());
        errorDetailResource.setTitle(e.getTitle().toString());
        errorDetailResource.setCode(e.getTitle().getCode());
        errorDetailResource.setDeveloperMessage(e.getClass().getName());
        errorDetailResource.setStatus(e.getStatus().value());
        errorDetailResource.setDetail(e.getMessage());
        return new ResponseEntity<>(errorDetailResource, e.getStatus());
    }
}

原因分析

Spring Cloud Gateway基于WebFlux响应式框架,而@ControllerAdvice是为Spring MVC同步控制器设计的,无法直接捕获WebFlux中GlobalFilter抛出的异常。此外,在响应式流中直接抛出异常(如map操作内)的方式不符合响应式编程规范,会导致异常无法被正确传递和捕获。

解决方案

1. 适配WebFlux的异常控制器

将@ControllerAdvice替换为@RestControllerAdvice,并将方法返回类型改为响应式的Mono<ResponseEntity<ErrorDetailResource>>,同时使用ServerWebExchange替代HttpServletRequest:

@RestControllerAdvice
public class ExceptionControllerImpl implements ExceptionController {

    @Override
    @ExceptionHandler(AuthorizationForbiddenException.class)
    public Mono<ResponseEntity<ErrorDetailResource>> handleGenericExceptions(
            AbstractBaseException e, ServerWebExchange exchange) {
        ErrorDetailResource errorDetailResource = new ErrorDetailResource();
        errorDetailResource.setTimestamp(Instant.now().toEpochMilli());
        errorDetailResource.setTitle(e.getTitle().toString());
        errorDetailResource.setCode(e.getTitle().getCode());
        errorDetailResource.setDeveloperMessage(e.getClass().getName());
        errorDetailResource.setStatus(e.getStatus().value());
        errorDetailResource.setDetail(e.getMessage());
        return Mono.just(new ResponseEntity<>(errorDetailResource, e.getStatus()));
    }
}

2. 修正GlobalFilter中的异常抛出方式

在响应式流中,不能直接抛出异常,需用Mono.error()包装异常;同时将map操作改为flatMap,以便正确传递异常信号:

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthFilter implements GlobalFilter {

    private final JwtTokenProviderService jwtTokenProviderService;
    private final TokenStatusDaoService tokenStatusDaoService;
    private final WebClient webClient;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("something in the way");
        List<String> headers = exchange.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION);
        if(CollectionUtils.isEmpty(headers)) {
            log.trace("Request came without token");
            return chain.filter(exchange);
        } else {
            String authToken = headers.get(0);
            log.trace("Request holds a token");
            log.debug("Check if token has expired ...");
            if(jwtTokenProviderService.isTokenExpired(authToken)) {
                log.debug("Token has expired will throw an error");
                // 用Mono.error包装异常,替代直接抛出
                return Mono.error(new AuthorizationForbiddenException(AuthorizationForbiddenExceptionTitleEnum.TOKEN_HAS_EXPIRED, "Token has expired"));

            }else {
                log.debug("Check if token is valid and already saved");
                String userId = jwtTokenProviderService.getClaimsFromToken(authToken).get(SecurityUtils.IDENTIFIER_KEY).toString();
                if(!tokenStatusDaoService.exists(TokenStatusSpecification.withToken(authToken).and(TokenStatusSpecification.withUserId(Long.parseLong(userId))))) {
                    return webClient.get()
                            .uri("http://uaa", uriBuilder -> uriBuilder
                                    .path("/validate-token")
                                    .queryParam("token", authToken).build()).retrieve()
                            .bodyToMono(TokenValidationGetResource.class)
                            // 改用flatMap,以便返回Mono.error传递异常
                            .flatMap(tokenValidationGetResource -> {
                                if (!tokenValidationGetResource.isValid()) {
                                    log.debug("token is not valid");
                                    return Mono.error(new AuthorizationForbiddenException(AuthorizationForbiddenExceptionTitleEnum.TOKEN_NOT_VALID, "Token is not valid"));
                                } else {
                                    log.debug("token is valid");
                                    TokenStatusEntity tokenStatusEntity;
                                    try {
                                        tokenStatusEntity = tokenStatusDaoService.findOne(TokenStatusSpecification.withUserId(Long.parseLong(userId)));
                                    } catch (Exception e) {
                                        log.debug("No token defined for user: {}. Will save a new one ...", userId);
                                        tokenStatusEntity = new TokenStatusEntity();
                                    }
                                    tokenStatusEntity.setToken(authToken);
                                    tokenStatusEntity.setUserId(Long.parseLong(userId));
                                    tokenStatusEntity.setStatus(TokenStatusEnum.VALID);
                                    tokenStatusDaoService.save(tokenStatusEntity);
                                    log.debug("Token status entity: {}", tokenStatusEntity);
                                    return Mono.just(exchange);
                                }
                            }).flatMap(chain::filter);
                } else {
                    log.debug("Token exists in DB");
                    return chain.filter(exchange);
                }
            }
        }
    }
}

3. 确认异常类继承关系

确保AuthorizationForbiddenException(或其父类AbstractBaseException)继承自RuntimeException,否则响应式流无法正确处理该异常。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 01:41:29