ElIauk

Spring Boot 拦截器与过滤器区别

两者不在同一层 "登录校验到底该写在 Filter 还是 Interceptor?"这个问题在面试和代码评审里都很常见。不少人觉得两者差不多,都是"请求进来先执行一段代码",于是随手选一个,后面遇到拿不到 Handler、注入不了 Bean、跨域预检被拦之类的问题,才发现选错了层。 简单说:Filt

评论
Spring Boot 拦截器与过滤器区别

两者不在同一层

"登录校验到底该写在 Filter 还是 Interceptor?"这个问题在面试和代码评审里都很常见。不少人觉得两者差不多,都是"请求进来先执行一段代码",于是随手选一个,后面遇到拿不到 Handler、注入不了 Bean、跨域预检被拦之类的问题,才发现选错了层。

简单说:Filter 属于 Servlet 规范,工作在 DispatcherServlet 之前;Interceptor 属于 Spring MVC,工作在 DispatcherServlet 内部、Handler 已经确定之后。层级不同,能拿到的东西和能做的事就不同。

一条请求经过谁

一个普通的 HTTP 请求进入 Tomcat 后,大致经过这些环节:

  1. 连接器接收请求,交给 FilterChain;
  2. 各个 Filter 依次执行 doFilter;
  3. 进入 DispatcherServlet,由 HandlerMapping 找到对应的 Controller 方法;
  4. 执行 HandlerInterceptor.preHandle;
  5. 通过 HandlerAdapter 调用 Controller;
  6. 执行 postHandle,此时 Controller 已经返回,视图或响应体还没真正写出;
  7. 执行 afterCompletion;
  8. 回到 Filter 链,执行 chain.doFilter 之后的代码。

顺序记成一句话:Filter 包着 DispatcherServlet,Interceptor 包着 Controller。所以 Filter 能改 HttpServletRequest、HttpServletResponse 本身,Interceptor 能拿到 HandlerMethod,知道"这次请求到底要进哪个方法"。

选择标准

需求 推荐 原因
统一编码、请求体包装 Filter 需要在最外层替换 request
traceId 写入 MDC Filter 要覆盖整个请求,包括 MVC 之外的异常
CORS 跨域 Filter 或 MVC 配置 预检请求不进入 Controller
登录、权限校验 Interceptor 能拿到 HandlerMethod 上的注解
接口耗时统计 Interceptor 能精确到方法级别
租户上下文切换 Interceptor 需要按注解或路径区分
响应压缩、XSS 过滤 Filter 面向原始请求/响应

有一个判断技巧:如果你需要"方法上的注解信息",选 Interceptor;如果你需要"在 Spring MVC 之外也生效",选 Filter。

还有一点容易被忽略:Filter 工作在 Spring MVC 之外,所以 Spring Security 这类框架本身就是以 Filter 链的形式接入的。项目里已经用了 Security,登录校验优先交给它,自己再叠加一个 Interceptor 容易和框架的上下文顺序打架。反过来,如果只是几个接口的按钮级权限判断,用 Interceptor 读方法上的注解更顺手。

各写一个

先看 Filter。用 OncePerRequestFilter 避免 forward/include 时重复执行:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String traceId = request.getHeader("X-Trace-Id");
        if (!StringUtils.hasText(traceId)) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }
        MDC.put("traceId", traceId);
        response.setHeader("X-Trace-Id", traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("traceId");
        }
    }
}

再看 Interceptor。它由 Spring 管理,可以正常构造注入依赖:

@Component
public class AuthInterceptor implements HandlerInterceptor {

    private final TokenService tokenService;

    public AuthInterceptor(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        if (!tokenService.valid(request.getHeader("Authorization"))) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
            return false;
        }
        return true;
    }
}

注册时只拦截业务接口,把登录和公开接口排除掉:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;

    public WebConfig(AuthInterceptor authInterceptor) {
        this.authInterceptor = authInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/auth/login", "/api/public/**");
    }
}

常见坑

1. 拦截器用 new 创建

registry.addInterceptor(new AuthInterceptor()); // 里面的 tokenService 是 null

手动 new 的对象不受 Spring 管理,@Autowired 不会生效。正确做法是把拦截器声明成 @Component 或 @Bean,再注入进配置类。

2. 跨域预检被拦截

浏览器发 OPTIONS 预检请求时,路径同样会经过拦截器。如果拦截器直接返回 401,浏览器控制台会报跨域失败。要么放行 OPTIONS,要么用 CorsFilter 在更外层处理。

3. 静态资源和错误页也被拦

/favicon.ico、/error、Swagger 静态资源都可能命中拦截器。除了 excludePathPatterns,还可以像上面的例子一样判断 handler instanceof HandlerMethod,非 Controller 方法直接放行。

4. Filter 读请求体后 Controller 读不到

request.getInputStream() 只能读一次。Filter 里为了打日志把 body 读掉,Controller 再读就是空字符串。要保留请求体,需要用 ContentCachingRequestWrapper 包装,并让下游从缓存的字节数组里取数据;如果只是记日志,记录长度、摘要或者不读 body 更省事。

5. 异步请求的回调不执行

Controller 返回 Callable、DeferredResult 或 WebFlux 时,postHandle 和 afterCompletion 的时机和同步请求不同。需要实现 AsyncHandlerInterceptor 的 afterConcurrentHandlingStarted,否则清理逻辑会漏掉。

6. 异常处理范围不同

Filter 里抛出的异常不经过 Spring MVC 的异常处理链,@RestControllerAdvice 通常接不住;Interceptor 在 preHandle 抛出的异常可以进入 HandlerExceptionResolver,但 afterCompletion 里的异常不会。所以鉴权失败时建议直接写响应并返回 false,而不是抛异常。

7. 顺序混乱

多个 Filter 的顺序用 @Order 或 FilterRegistrationBean 控制;多个 Interceptor 的顺序在注册时用 .order(n) 控制。preHandle 按正序执行,postHandle 和 afterCompletion 按逆序执行,日志里看到的顺序和直觉可能相反。

排查步骤

出现"该拦的没拦、不该拦的拦了",先打一行日志确认执行顺序:

log.info("filter={} uri={}", this.getClass().getSimpleName(), request.getRequestURI());

然后按路径确认:请求有没有命中 addPathPatterns,有没有被 excludePathPatterns 排除,handler 是不是 HandlerMethod。如果涉及跨域,用 curl -X OPTIONS -i 单独测预检;如果涉及响应,看是不是在 chain.doFilter 之前就把 response 提交了。定位清楚层级,再决定把逻辑放在 Filter 还是 Interceptor。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者