Springboot中一般使用ResponseEntityExceptionHandler统一异常处理,封装到ReplyVO中。
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
public static final String MSG_ACCESS_DENIED_EXCEPTION = "权限受限无法访问";
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ReplyVO<String>> accessDeniedException(AccessDeniedException ex) {
return getResponseEntityReplyVO(MSG_ACCESS_DENIED_EXCEPTION, HttpStatus.PAYMENT_REQUIRED);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ReplyVO<String>> handleIllegalArgumentException(IllegalArgumentException ex) {
return getResponseEntityReplyVO(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
/**
* 获取响应实体回复VO
*
* @param strMessage 消息
* @param httpStatus http状态
* @return {@link ResponseEntity }<{@link ReplyVO }<{@link String }>>
*/
private ResponseEntity<ReplyVO<String>> getResponseEntityReplyVO(String strMessage, HttpStatus httpStatus) {
return new ResponseEntity<>(new ReplyVO<>(strMessage, ReplyEnum.ERROR_SERVER_ERROR), new HttpHeaders(), httpStatus);
}
}
但是在处理JWT时,需要配置@EnableWebSecurity的,将JWT的过滤器放到过LogoutFilter前面,这样就造成JWT验证过程中抛出的BadCredentialsException无法在ResponseEntityExceptionHandler中统一处理。
为实现统一返回统一格式的JSON结构,需要对BadCredentialsException单独进行处理
/**
* JWT请求过滤
*/
private final JwtRequestFilter jwtRequestFilter;
/**
* 安全过滤链
*
* @param httpSecurity http安全性
* @return {@link SecurityFilterChain}
* @throws Exception 异常
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
......
// 在过滤器之前加入JWT请求过滤器
httpSecurity.addFilterBefore(jwtRequestFilter, LogoutFilter.class);
......
}
配置过滤器验证JWT签名的有效性JwtRequestFilter.java,错误的签名抛出BadCredentialsException异常,且catch之后需要调用AuthenticationEntryPoint向response中注入内容。这样我们就可以对BadCredentialsException异常统一进行处理
/**
* JWT请求过滤
*
* @author simen
* @date 2022/02/10
*/
@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
public static final String MSG_LOGIN_TOKEN_NOT_EXIST = "用户登录信息不存在";
public static final String MSG_SIGNATURE_FAILED = "用户签名验证失败";
public static final String MSG_USER_SIGNATURE_EXPIRED = "用户签名已过期";
final AuthenticationEntryPoint authenticationEntryPoint;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
......
// 判断token是否存在
if (StrUtil.isBlank(strToken)) {
throw new BadCredentialsException(MSG_LOGIN_TOKEN_NOT_EXIST);
}
// 校验签名
try {
if (!JWTUtil.verify(strToken, SM2_JWT_SIGNER)) {
throw new BadCredentialsException(MSG_SIGNATURE_FAILED);
}
} catch (Exception e) {
throw new BadCredentialsException(MSG_SIGNATURE_FAILED);
}
......
} catch (AuthenticationException exception) {
securityContextHolderStrategy.clearContext();
authenticationEntryPoint.commence(request, response, exception);
return;
}
// 使用过滤链进行过滤
filterChain.doFilter(request, response);
}
}
JwtAuthenticationEntryPoint.java对应authenticationEntryPoint.commence(request, response, exception),将所有的异常封装到ReplyVO中
/**
* JWT认证入口点
*
* @author Simen
* @date 2022/02/10
*/
@Slf4j
@Component("JwtAuthenticationEntryPoint")
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
// 从request头中获取Accept
String strAccept = request.getHeader("Accept");
if (StrUtil.isNotBlank(strAccept)) {
// 对Accept分组为字符串数组
String[] strsAccept = StrUtil.splitToArray(strAccept, ",");
// 判断Accept数组中是否存在"text/html"
if (ArrayUtil.contains(strsAccept, "text/html")) {
// 存在"text/html",判断为html访问,则跳转到登录界面
response.sendRedirect(STR_URL_LOGIN_URL);
} else {
// 不存在"text/html",判断为json访问,则返回未授权的json
SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK,
new ReplyVO<>(authException.getMessage(), ReplyEnum.ERROR_SERVER_ERROR));
}
}
}
}