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
相关产品推荐
相关产品推荐

