這兩天在做 spring cloud 的 API gateway 的時候,遇到了一個全域性錯誤處理的坑,我在在 spring security 中加入了一個 filter,該 filter 用來驗證 token 是否合法,如果該 token 不合法,就丟擲自定義錯誤 InvalidTokenException ,並且要返回的狀態碼為403,告訴前端該使用者未認證。
我開始以為使用 @ControllerAdvice
來定義全域性錯誤處理即可,最後發現過濾器中丟擲了錯誤 @ControllerAdvice
卻無法捕獲到,並丟擲我需要的錯誤資訊。最後閱讀 spring 的官方文件發現,spring 的全域性錯誤處理不是隻有 @ControllerAdvice
。
@ControllerAdvice
主要處理的就是 controller
層的錯誤資訊,而沒有進入 controller
層的錯誤 @ControllerAdvice
是無法處理的,那麼我需要另外的一個全域性錯誤處理。
@ControllerAdvice
public class ExceptionTranslator {
@ExceptionHandler(ConcurrencyFailureException.class)
@ResponseStatus(HttpStatus.CONFLICT)
@ResponseBody
public ErrorVM processConcurencyError(ConcurrencyFailureException ex) {
return new ErrorVM(ErrorConstants.ERR_CONCURRENCY_FAILURE);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorVM processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
@ExceptionHandler(CustomParameterizedException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ParameterizedErrorVM processParameterizedValidationError(CustomParameterizedException ex) {
return ex.getErrorVM();
}
@ExceptionHandler(InvalidTokenException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
@ResponseBody
public ErrorVM processInvalidTokenException(InvalidTokenException ex) {
return new ErrorVM(ErrorConstants.INVALID_TOKEN, ex.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
@ResponseBody
public ErrorVM processAccessDeniedException(AccessDeniedException e) {
return new ErrorVM(ErrorConstants.ERR_ACCESS_DENIED, e.getMessage());
}
private ErrorVM processFieldErrors(List<FieldError> fieldErrors) {
ErrorVM dto = Objects.nonNull(fieldErrors.get(0)) ? new ErrorVM(ErrorConstants.ERR_VALIDATION, fieldErrors.get(0).getDefaultMessage()) : new ErrorVM(ErrorConstants.ERR_VALIDATION);
for (FieldError fieldError : fieldErrors) {
dto.add(fieldError.getObjectName(), fieldError.getField(), fieldError.getCode());
}
return dto;
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseBody
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ErrorVM processMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) {
return new ErrorVM(ErrorConstants.ERR_METHOD_NOT_SUPPORTED, exception.getMessage());
}
@ExceptionHandler(CustomException.class)
@ResponseBody
@ResponseStatus(HttpStatus.IM_USED)
public ErrorVM processCustomException(CustomException ex) {
return new ErrorVM(ErrorConstants.ERR_CUSTOM, ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorVM> processRuntimeException(Exception ex) {
BodyBuilder builder;
ErrorVM errorVM;
ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
if (responseStatus != null) {
builder = ResponseEntity.status(responseStatus.value());
errorVM = new ErrorVM("error." + responseStatus.value().value(), responseStatus.reason());
} else {
builder = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR);
errorVM = new ErrorVM(ErrorConstants.ERR_INTERNAL_SERVER_ERROR, "Internal server error");
}
return builder.body(errorVM);
}
}
BasicErrorController
這個類就是用來捕獲 /error
的所有錯誤,而過濾器中的錯誤會被重定向到 /error
。我編寫一個新的控制層類 TokenErrorController
並繼承 BasicErrorController
類,這樣錯誤 json 型別的錯誤都會被重定向到這個控制層裡。
@RestController
public class TokenErrorController extends BasicErrorController {
public TokenErrorController(){
super(new DefaultErrorAttributes(), new ErrorProperties());
}
private static final String PATH = "/error";
@RequestMapping(produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
if (!Strings.isNullOrEmpty((String)body.get("exception")) && body.get("exception").equals(InvalidTokenException.class.getName())){
body.put("status", HttpStatus.FORBIDDEN.value());
status = HttpStatus.FORBIDDEN;
}
return new ResponseEntity<Map<String, Object>>(body, status);
}
@Override
public String getErrorPath() {
return PATH;
}
}