前言:
在改造老项目登录功能的时候,使用了过滤器对 token 进行有效性验证,验证通过继续进行业务请求,验证不通过则抛出校验异常。
过程:
技术方案拟定后,就着手开始改造,一切都很顺畅,可是在异常场景模拟的时候,怎么也得不到想要的异常 code,我在过滤器的校验中明明是抛出了异常,为什么没有得到想要的结果呢?
过滤器代码如下:
java
@Slf4j
//@WebFilter(filterName = "myFilter", urlPatterns = "/*")
public class MyAuthenticationFilter implements Filter {
//不拦截的 URL
private final static String EXCLUDES_URI = "/api/workflow/*";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//为了方便获取 header 信息 对 HttpServletRequest 进行强转
HttpServletRequest request = (HttpServletRequest) servletRequest;
String requestUri = request.getRequestURI();
//需要忽略的 url 地址
Pattern pattern = Pattern.compile(EXCLUDES_URI);
//是否放行
boolean isExclude = pattern.matcher(requestUri).find();
//用户信息
UserInfoVO userInfoVO = null;
if (!isExclude) {
//校验认证信息
userInfoVO = validateAuthorization(request);
if (ObjectUtil.isNull(userInfoVO)) {
//校验认证信息 失败 可能解析 token 异常 可能没有解析到正确的工号
throw new AuthorizationValidationException(ResultCode.CAS_AUTHORIZATION);
}
}
//设置用户信息
UserContextHolder.setUser(userInfoVO);
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
//将ThreadLocal数据清空
UserContextHolder.remove();
Filter.super.destroy();
}
/**
* @Description: 校验 Authorization
* @Date: 2024/4/3 10:25
*/
public UserInfoVO validateAuthorization(HttpServletRequest request) {
//获取 Authorization
String authorization = request.getHeader(CommConstant.AUTHORIZATION);
if (StringUtils.isBlank(authorization)) {
StringBuffer requestUrl = request.getRequestURL();
log.info("Authorization 为空的请求url:{}", requestUrl);
//Authorization 为空 没有登录
return null;
}
//过滤器是 servlet 规范中定义的 不归 spring 容器管理 无法直接注入 spring 中的 bean 直接注入会为 null
//只能够自己去 容器中获取 bean
ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
assert context != null;
AuthServiceImpl iAuthService = (AuthServiceImpl) context.getBean("authServiceImpl");
//检验 authorization
return iAuthService.validateToken(authorization);
}
}
配置过滤器代码:
java
package com.zt.zteam.main.configurer;
import com.zt.zteam.main.filter.MyAuthenticationFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyAuthenticationFilterConfig {
@Bean("myFilterAuthentication")
public FilterRegistrationBean<MyAuthenticationFilter> filterAuthenticationRegistration() {
//设置过滤器
FilterRegistrationBean<MyAuthenticationFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new MyAuthenticationFilter());
//设置过滤器优先级 数字越小优先级越高
registration.setOrder(-1);
return registration;
}
}
配置过滤方式二:
过滤器类上加 @WebFilter(filterName = "myFilter", urlPatterns = "/*") 注解,同时在启动类上加 @ServletComponentScan({"com.xxx.xxx.xxx.filter"}) 注解。
全局异常处理器代码如下:
java
@RestControllerAdvice(basePackages = "com.my.study")
//@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class GlobalExceptionHandler {
/**
* 交易异常
*/
@ExceptionHandler(ValidateException.class)
public Result<?> validateExceptionHandler(HttpServletRequest request, ValidateException e) {
log.error("校验异常,方法:{}", request.getRequestURI(), e);
return ResultGenerator.genResult(e.getCode(), e.getMessage());
}
/**
* 处理业务异常
*/
@ExceptionHandler(ServiceException.class)
public Result<?> bizExceptionHandler(HttpServletRequest request, ServiceException e) {
log.error("业务异常,方法:{}", request.getRequestURI(), e);
return ResultGenerator.genResult(e.getCode(), e.getMessage());
}
/**
* 处理空指针异常
*/
@ExceptionHandler(NullPointerException.class)
public Result<?> npeHandler(HttpServletRequest request, NullPointerException e) {
log.error("空指针异常,方法:{}", request.getRequestURI(), e);
return ResultGenerator.genFailResult(e.getMessage());
}
/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
public Result<?> exceptionHandler(HttpServletRequest request, Exception e) {
log.error("其它异常,方法:{}", request.getRequestURI(), e);
Result<?> result = new Result<>();
result.setCode(ResultCode.INTERNAL_SERVER_ERROR).setMessage(e.getMessage() + "--接口 [" + request.getRequestURI() + "] 内部错误,请联系管理员");
return result;
}
@ExceptionHandler({BusinessException.class})
public Result<?> businessException(HttpServletRequest request, Exception e) {
log.error("业务处理异常信息,方法:{},异常信息:" ,request.getRequestURI(), e);
Result<?> result = new Result<>();
result.setCode(ResultCode.INTERNAL_SERVER_ERROR).setMessage(e.getMessage());
return result;
}
/**
* 这个是valid注解校验参数时,校验不通过的异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> validateMethodExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException e) {
log.error("请求参数异常,方法:{}", request.getRequestURI(), e);
Result<?> result = new Result<>();
result.setCode(ResultCode.INTERNAL_SERVER_ERROR).setMessage(e.getBindingResult().getFieldErrors().get(0).getDefaultMessage());
return result;
}
/**
* token 认证验证
*/
@ExceptionHandler(AuthorizationValidationException.class)
public Result<?> authorizationValidationExceptionHandler(HttpServletRequest request, Exception e) {
log.error("CAS 认证异常,方法:{}", request.getRequestURI(), e);
Result<?> result = new Result<>();
result.setCode(ResultCode.UNAUTHORIZED).setMessage(e.getMessage() + "--接口 [" + request.getRequestURI() + "] 内部错误,请联系管理员");
return result;
}
}
测试结果:
powershell
{
"timestamp": "2024-04-16T02:04:48.091+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/api/logout"
}
没有得到我们预期的 code:401。
问题分析:
看起来代码显示的抛出了异常,也设置了全局异常处理器,但是并没有返回想要的异常状态码,至此感觉走到了死胡同,此时想到了老办法,debug 调试,经过多次 debug 调试,发现全局异常处理器没有拦截到任何异常,这就很能说明问题了,也就是全局异常处理器,根本捕获不到过滤器 filter 抛出的异常,那怎么办呢?我们知道全局异常过滤器是一定可以捕获到 Controller 的异常的,此时灵机一动,当出现异常后,在过滤器 filter 中使用 try catch 自己处理,然后使用 forward 转发请求到指定 Controller 不就可以了吗,方案有了,着手开始测试。
注意:@ControllerAdvice 注解只处理经过 Controller 的异常,不经过 Controller 的异常 @ControllerAdvice 注解不进行处理。
调整后的过滤器 filter 代码如下:
java
@Slf4j
public class MyAuthenticationFilter implements Filter {
//不拦截的 URL
private final static String EXCLUDES_URI = "/api/workflow/*";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//为了方便获取 header 信息 对 HttpServletRequest 进行强转
HttpServletRequest request = (HttpServletRequest) servletRequest;
String requestUri = request.getRequestURI();
//需要忽略的 url 地址
Pattern pattern = Pattern.compile(EXCLUDES_URI);
//是否放行
boolean isExclude = pattern.matcher(requestUri).find();
//用户信息
UserInfoVO userInfoVO = null;
try {
if (!isExclude) {
//校验认证信息
userInfoVO = validateAuthorization(request);
if (ObjectUtil.isNull(userInfoVO)) {
//校验认证信息 失败 可能解析 token 异常 可能没有解析到正确的工号
throw new AuthorizationValidationException(ResultCode.CAS_AUTHORIZATION);
}
}
//设置用户信息
UserContextHolder.setUser(userInfoVO);
filterChain.doFilter(servletRequest, servletResponse);
} catch (AuthorizationValidationException e) {
request.setAttribute(CommConstant.FILTER_ERROR, e);
request.getRequestDispatcher(CommConstant.FILTER_ERROR_PATH).forward(request, servletResponse);
} catch (IOException e) {
e.printStackTrace();
} catch (ServletException e) {
e.printStackTrace();
}
}
@Override
public void destroy() {
//将ThreadLocal数据清空
UserContextHolder.remove();
Filter.super.destroy();
}
/**
* @Description: 校验 Authorization
* @Date: 2024/4/3 10:25
*/
public UserInfoVO validateAuthorization(HttpServletRequest request) {
//获取 Authorization
String authorization = request.getHeader(CommConstant.AUTHORIZATION);
if (StringUtils.isBlank(authorization)) {
StringBuffer requestUrl = request.getRequestURL();
log.info("Authorization 为空的请求url:{}", requestUrl);
//Authorization 为空 没有登录
return null;
}
//过滤器是 servlet 规范中定义的 不归 spring 容器管理 无法直接注入 spring 中的 bean 直接注入会为 null
//只能够自己去 容器中获取 bean
ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
assert context != null;
AuthServiceImpl iAuthService = (AuthServiceImpl) context.getBean("authServiceImpl");
//检验 authorization
return iAuthService.validateToken(authorization);
}
}
请求转发的关键代码:
java
//filterError
public final static String FILTER_ERROR = "filterError";
//filterError path
public final static String FILTER_ERROR_PATH = "/throw-error";
//设置异常信息
request.setAttribute(CommConstant.FILTER_ERROR, e);
//转发
request.getRequestDispatcher(CommConstant.FILTER_ERROR_PATH).forward(request, servletResponse);
异常处理 Controller 代码:
java
@Slf4j
@RestController
public class FilterExceptionController {
@ApiOperation(value = "过滤器异常处理", produces = "application/json")
@RequestMapping(CommConstant.FILTER_ERROR_PATH)
public Result<String> testRedis(HttpServletRequest request) {
Object attribute = request.getAttribute(CommConstant.FILTER_ERROR);
if(attribute instanceof AuthorizationValidationException){
throw new AuthorizationValidationException(ResultCode.CAS_AUTHORIZATION);
}
throw new BusinessException("业务异常");
}
}
测试结果:
总结:
通过请求转发的方式,我们解决了过滤器 filter 异常无法捕获的问题,在转发的过程中我们尽量使用 request.getRequestDispatcher("/path").forward(request, response) 这种方式,此方式只会在服务端内部转发,客户端地址不会发生任何改变,如果使用response.sendRedirect("/path") 进行请求转发,客户端地址会发生改变。
在 Spring 应用中我们不建议优先使用过滤器 filter,建议优先使用拦截器 Interceptor,本文只是分享过滤器 filter 中的异常处理方式,希望帮助到有需要的伙伴们。
过滤器和拦截器的区别传送门:
Spring 拦截器实现请求拦截与参数处理【拦截器(Interceptor)和过滤器(Filter)的区别】
如有错误的地方欢迎指出纠正。