Spring Boot 处理过滤器(filter )中抛出的异常

前言:

在改造老项目登录功能的时候,使用了过滤器对 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)的区别】

如有错误的地方欢迎指出纠正。

相关推荐
容若只如初见1 小时前
项目实战--Spring Boot + Minio文件切片上传下载
java·spring boot·后端
weixin_440401692 小时前
分布式锁——基于Redis分布式锁
java·数据库·spring boot·redis·分布式
码农爱java2 小时前
Spring Boot 中的监视器是什么?有什么作用?
java·spring boot·后端·面试·monitor·监视器
无名指的等待7123 小时前
SpringBoot实现图片添加水印(完整)
java·spring boot·后端
胡尚3 小时前
Ratf协议图解、Nacos CP集群源码分析
java·spring boot
三两肉5 小时前
如何使用缓存提升SpringBoot性能(EhCache和Redis方式)
spring boot·redis·缓存
无知的小菜鸡5 小时前
SSM学习4:spring整合mybatis、spring整合Junit
spring·mybatis
一路向北·重庆分伦6 小时前
03:Spring MVC
java·spring·mvc
甜甜圈的小饼干6 小时前
Spring Boot+Vue项目从零入手
vue.js·spring boot·后端
踩着阴暗的自己向上爬7 小时前
Day02-Jenkins与集成案例
linux·运维·servlet·jenkins