一、背景
系統定義了全局統一異常處理,使用了@RestControllerAdvice註解的方式。
package com.ruoyi.framework.web.exception;
import com.ruoyi.common.exception.BusinessException;
import com.ruoyi.common.exception.DemoModeException;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.security.PermissionUtils;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.jwt.exception.CustomException;
import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
/**
* 全局異常處理器
*
* @author ruoyi
*/
@RestControllerAdvice
public class GlobalExceptionHandler
{
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 權限校驗失敗 如果請求爲ajax返回json,普通請求跳轉頁面
*/
@ExceptionHandler(AuthorizationException.class)
public Object handleAuthorizationException(HttpServletRequest request, AuthorizationException e)
{
log.error(e.getMessage(), e);
if (ServletUtils.isAjaxRequest(request))
{
return AjaxResult.error(PermissionUtils.getMsg(e.getMessage()));
}
else
{
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error/unauth");
return modelAndView;
}
}
/**
* 請求方式不支持
*/
@ExceptionHandler({ HttpRequestMethodNotSupportedException.class })
public AjaxResult handleException(HttpRequestMethodNotSupportedException e)
{
log.error(e.getMessage(), e);
return AjaxResult.error("不支持' " + e.getMethod() + "'請求");
}
/**
* 業務異常
*/
@ExceptionHandler(CustomException.class)
public AjaxResult customException(CustomException e)
{
if (StringUtils.isNull(e.getCode()))
{
return AjaxResult.error(e.getMessage());
}
return AjaxResult.error(String.valueOf(e.getCode()), e.getMessage());
}
/**
* 攔截未知的運行時異常
*/
@ExceptionHandler(RuntimeException.class)
public AjaxResult notFount(RuntimeException e)
{
log.error("運行時異常:", e);
return AjaxResult.error("運行時異常:" + e.getMessage());
}
/**
* 系統異常
*/
@ExceptionHandler(Exception.class)
public AjaxResult handleException(Exception e)
{
log.error(e.getMessage(), e);
return AjaxResult.error("服務器錯誤,請聯繫管理員");
}
/**
* 業務異常
*/
@ExceptionHandler(BusinessException.class)
public Object businessException(HttpServletRequest request, BusinessException e)
{
log.error(e.getMessage(), e);
if (ServletUtils.isAjaxRequest(request))
{
return AjaxResult.error(e.getMessage());
}
else
{
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("errorMessage", e.getMessage());
modelAndView.setViewName("error/business");
return modelAndView;
}
}
/**
* 自定義驗證異常
*/
@ExceptionHandler(BindException.class)
public AjaxResult validatedBindException(BindException e)
{
log.error(e.getMessage(), e);
String message = e.getAllErrors().get(0).getDefaultMessage();
return AjaxResult.error(message);
}
/**
* 演示模式異常
*/
@ExceptionHandler(DemoModeException.class)
public AjaxResult demoModeException(DemoModeException e)
{
return AjaxResult.error("演示模式,不允許操作");
}
}
鑑權使用的是JWT,使用filter對token進行解析和驗證。然後突然發現一個問題,就是當token過期或j解密失敗時,拋出的異常無法通過全局統一異常處理對外返回,而是返回500。
二、原因
全局統一異常處理只能處理控制器中發生的異常。要在Spring Security過濾器鏈中重用此功能,需要定義過濾器並將其掛鉤到安全配置中。過濾器需要將異常重定向到統一異常處理中。
三、代碼
在filter中注入HandlerExceptionResolver
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
然後在catch中拋出
//有異常就是token解析失敗
resolver.resolveException(req, resp, null, new CustomException(BizExceptionEnum.SIGN_ERROR.getMessage(),BizExceptionEnum.SIGN_ERROR.getCode()));
return;
AuthFilter全部代碼如下:
package com.ruoyi.jwt.filter;
import com.ruoyi.jwt.config.JwtConfig;
import com.ruoyi.jwt.config.JwtProperties;
import com.ruoyi.jwt.exception.BizExceptionEnum;
import com.ruoyi.jwt.exception.CustomException;
import com.ruoyi.jwt.util.JwtTokenUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class AuthFilter implements Filter {
private final Log logger = LogFactory.getLog(this.getClass());
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
public List<String> excludes = new ArrayList<>();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String temp = filterConfig.getInitParameter("excludes");
if (temp != null) {
String[] url = temp.split(",");
for (int i = 0; url != null && i < url.length; i++) {
excludes.add(url[i]);
}
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//axios 跨域會發送預請求,直接通過
if (req.getMethod().equals("OPTIONS")) {
filterChain.doFilter(request, response);
}
if(handleExcludeURL(req, resp)){
filterChain.doFilter(request, response);
return;
}
JwtProperties.Config config = JwtConfig.getJwtconfig();
final String requestHeader = req.getHeader(config.getHeader());
String authToken = "";
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
authToken = requestHeader.substring(7);
//驗證token是否過期,包含了驗證jwt是否正確
try {
boolean flag = jwtTokenUtil.isTokenExpired(authToken, config);
if (flag) {
resolver.resolveException(req, resp, null, new CustomException(BizExceptionEnum.TOKEN_ERROR.getMessage(),BizExceptionEnum.TOKEN_ERROR.getCode()));
return;
}
} catch (Exception e) {
//有異常就是token解析失敗
resolver.resolveException(req, resp, null, new CustomException(BizExceptionEnum.SIGN_ERROR.getMessage(),BizExceptionEnum.SIGN_ERROR.getCode()));
return;
}
} else {
//header沒有帶Bearer字段
resolver.resolveException(req, resp, null, new CustomException(BizExceptionEnum.TOKEN_EXPIRED.getMessage(),BizExceptionEnum.TOKEN_EXPIRED.getCode()));
return;
}
filterChain.doFilter(request, response);
}
private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) {
if (excludes == null || excludes.isEmpty()) {
return false;
}
String url = request.getServletPath();
for (String pattern : excludes) {
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(url);
if (m.find()) {
return true;
}
}
return false;
}
@Override
public void destroy() {}
}
四、備註
參考了https://stackoverflow.com/questions/34595605/how-to-manage-exceptions-thrown-in-filters-in-spring 中的回答