原创

Spring Boot拦截器与过滤器:区别、实现与实战指南

为什么 Filter 和 Interceptor 容易被混淆

在 Spring Boot 项目里,二者都能“拦请求”,都能做日志、鉴权、审计、限流前置处理,所以很多人把它们当成同一类东西使用。真正的区别不在“能不能拦”,而在拦截位置、依赖体系、可见上下文、适用目标。

一句话先定性:

  • Filter 是 Servlet 规范层 的组件,工作在整个 Web 请求链更靠前的位置
  • Interceptor 是 Spring MVC 层 的组件,工作在 DispatcherServlet 分发之后、Controller 执行之前后

这意味着它们解决的问题并不完全相同。


先看执行链路

一次典型 HTTP 请求在 Spring Boot MVC 应用中的简化流程如下:

客户端请求
   ↓
Servlet Container
   ↓
Filter 链
   ↓
DispatcherServlet
   ↓
HandlerMapping
   ↓
Interceptor.preHandle()
   ↓
Controller
   ↓
Interceptor.postHandle()
   ↓
视图渲染 / 返回响应体
   ↓
Interceptor.afterCompletion()
   ↓
Filter 返回
   ↓
响应客户端

如果是异常场景,afterCompletion() 仍然会执行;如果是异步请求,执行时机还会更复杂,这也是很多线上问题的来源之一。


核心区别:不是谁更高级,而是谁离请求更近

对比项 Filter Interceptor
所属体系 Servlet 规范 Spring MVC
生效位置 DispatcherServlet 之前 DispatcherServlet 之后
作用目标 进入当前 Servlet 链的请求 进入 Spring MVC 处理链的请求
是否依赖 Spring MVC 不依赖 依赖
典型接口 Filter / OncePerRequestFilter HandlerInterceptor
可拿到 Controller 信息 拿不到 能拿到 handler,可判断具体处理器
是否适合改写 Request/Response 非常适合 不适合做底层包装
是否适合统一编码、跨域、请求体包装 适合 不适合
是否适合基于业务接口的权限控制 一般 更适合
顺序控制方式 Filter 注册顺序 / @Order / FilterRegistrationBean addInterceptors 注册顺序
与 Spring 异常体系集成 弱 强
适合范围 更底层、更通用 更贴近业务路由

最容易记住的判断方式是:

  • 凡是跟 Servlet 请求/响应对象本身强相关的,优先考虑 Filter
  • 凡是跟 Controller、接口权限、业务上下文强相关的,优先考虑 Interceptor

Filter 深入解析

Filter 的本质

Filter 来自 Servlet 规范,不是 Spring MVC 发明的。它在请求进入 DispatcherServlet 之前执行,所以它更像一个 Web 基础设施组件。

常见场景:

  • 统一字符编码
  • CORS 处理
  • 请求日志打点
  • traceId 注入
  • 请求体包装
  • XSS 过滤
  • GZIP 压缩
  • 黑名单 IP 拦截
  • 对请求和响应做底层增强

Spring Boot 中虽然可以直接实现 jakarta.servlet.Filter,但更推荐继承 OncePerRequestFilter,因为它能避免同一次请求在某些派发场景下被重复执行。


Spring Boot 3.x 中实现一个 Filter

以下示例基于 Spring Boot 3.x / Spring Framework 6.x 该版本下 Servlet API 包名为 jakarta.servlet.*,不是 javax.servlet.*

package com.example.demo.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.UUID;

@Component
public class TraceIdFilter extends OncePerRequestFilter {

    public static final String TRACE_ID = "traceId";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        String traceId = request.getHeader("X-Trace-Id");
        if (traceId == null || traceId.isBlank()) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }

        request.setAttribute(TRACE_ID, traceId);
        response.setHeader("X-Trace-Id", traceId);
        MDC.put(TRACE_ID, traceId);

        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.remove(TRACE_ID);
        }
    }
}

这个 Filter 做了三件事:

  1. 从请求头读取 X-Trace-Id
  2. 如果没有就生成一个
  3. 放入 request、响应头和日志上下文 MDC

这种事情用 Interceptor 也能做,但在请求链更前面做更合理,因为后续所有组件都可能需要它。


精确控制 Filter 的生效范围和顺序

如果你不想让 Filter 拦所有请求,或者要控制多个 Filter 顺序,使用 FilterRegistrationBean。

package com.example.demo.config;

import com.example.demo.filter.TraceIdFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<TraceIdFilter> traceIdFilterRegistration(TraceIdFilter traceIdFilter) {
        FilterRegistrationBean<TraceIdFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(traceIdFilter);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(1);
        registrationBean.setName("traceIdFilter");
        return registrationBean;
    }
}

这里需要注意两点:

  • order 越小,越早执行
  • urlPatterns 是 Servlet 风格匹配,不是 Spring MVC 的 Ant 路径匹配

Filter 更适合处理“请求对象本身”

比如你想记录请求体,但 HttpServletRequest 输入流默认只能读一次。这类问题必须在更底层解决,通常通过包装请求对象实现。

package com.example.demo.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Component
public class RequestResponseLogFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

        long start = System.currentTimeMillis();
        try {
            filterChain.doFilter(requestWrapper, responseWrapper);
        } finally {
            long cost = System.currentTimeMillis() - start;

            String requestBody = new String(
                    requestWrapper.getContentAsByteArray(),
                    StandardCharsets.UTF_8
            );

            String responseBody = new String(
                    responseWrapper.getContentAsByteArray(),
                    StandardCharsets.UTF_8
            );

            System.out.println("URI=" + request.getRequestURI()
                    + ", cost=" + cost + "ms"
                    + ", req=" + requestBody
                    + ", resp=" + responseBody);

            responseWrapper.copyBodyToResponse();
        }
    }
}

这里最后一行 copyBodyToResponse() 不能漏。漏掉后,客户端可能收不到响应体。这是一个非常常见的坑。


Interceptor 深入解析

Interceptor 的本质

Interceptor 属于 Spring MVC,请求已经进入 DispatcherServlet,并且已经找到了候选处理器,此时它才有机会参与。

它最关键的价值不是“也能拦请求”,而是它能感知 Spring MVC 语义:

  • 当前匹配的是哪个 Handler
  • 能不能放行到 Controller
  • Controller 执行完之后是否还要处理
  • 请求结束后是否做资源清理
  • 能和 Spring MVC 的参数绑定、异常处理、返回值处理逻辑协同

因此它特别适合做:

  • 登录态校验
  • 基于接口粒度的权限控制
  • 幂等校验
  • 接口访问审计
  • 针对特定 Controller 的埋点
  • 接口级别限流前置校验
  • 国际化、租户、用户上下文注入

HandlerInterceptor 的三个核心方法

public interface HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object handler,
                                 Exception ex) throws Exception {
    }
}

preHandle

在 Controller 执行之前调用。

返回值含义非常直接:

  • true:放行,继续执行后续拦截器和 Controller
  • false:中断请求链,不再进入 Controller

postHandle

Controller 正常执行完成后、视图渲染之前调用。

适合:

  • 给 ModelAndView 补充公共数据
  • 补充响应级日志信息

对于 @ResponseBody / REST 接口,它的使用价值通常没那么大。

afterCompletion

请求结束后调用,常用于:

  • 资源释放
  • 清理 ThreadLocal
  • 记录最终异常信息
  • 记录完整耗时

Spring Boot 中实现一个 Interceptor

下面做一个最常见的登录校验示例。

package com.example.demo.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        String token = request.getHeader("Authorization");
        if (token == null || token.isBlank()) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write("""
                {"code":401,"message":"未登录或 token 缺失"}
                """);
            return false;
        }

        return true;
    }
}

然后注册到 MVC 配置中:

package com.example.demo.config;

import com.example.demo.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final LoginInterceptor loginInterceptor;

    public WebMvcConfig(LoginInterceptor loginInterceptor) {
        this.loginInterceptor = loginInterceptor;
    }

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

这个配置体现了 Interceptor 最有价值的能力:按业务接口路径精确控制拦截范围。


handler 参数为什么重要

很多时候,Interceptor 比 Filter 更适合做业务鉴权,不是因为它“更 Spring”,而是因为它能拿到 handler。

if (handler instanceof HandlerMethod handlerMethod) {
    Class<?> beanType = handlerMethod.getBeanType();
    String methodName = handlerMethod.getMethod().getName();
    System.out.println("当前请求命中 Controller: " + beanType.getName() + "#" + methodName);
}

这意味着你可以做到:

  • 只拦带某个注解的方法
  • 不同 Controller 使用不同鉴权策略
  • 记录接口维度审计日志
  • 识别匿名接口与内部接口

这类能力在 Filter 中天然做不到。


一个容易被说错的点:Interceptor 并不等于“只拦 Controller”

更准确的说法是:

  • Filter 作用于 Servlet 请求链
  • Interceptor 作用于 Spring MVC 的 HandlerExecutionChain

只要请求进入 Spring MVC 并且匹配到某个 Handler,就可能被 Interceptor 处理。 在 Spring Boot 默认静态资源配置下,一部分静态资源请求也是由 Spring MVC 的资源处理器接管的,因此Interceptor 是否会处理静态资源,取决于资源是否经过 Spring MVC Handler 链以及你的路径配置。

所以不要简单记成“Filter 拦所有,Interceptor 只拦 Controller”,这个说法不够准确。


实战建议:日志、鉴权、上下文注入应该怎么分层

项目里最常见的问题不是不会写,而是职责放错层。

一个稳定的分层方式通常是:

放到 Filter 的事情

  • 生成或透传 traceId
  • CORS
  • 请求/响应包装
  • 原始请求日志
  • 编码处理
  • IP 黑白名单
  • 基础安全头写入

放到 Interceptor 的事情

  • 用户登录态校验
  • RBAC 权限判断
  • 接口幂等校验
  • 多租户上下文注入
  • 接口级审计
  • 基于注解的业务规则拦截

这套分层的本质是:

  • Filter 解决“请求进入系统时”的基础设施问题
  • Interceptor 解决“请求进入具体业务接口前后”的业务治理问题

组合实战:Filter 负责 traceId,Interceptor 负责登录校验

这是企业项目里非常常见、也非常合理的组合。

第一步:Filter 写入 traceId

前面已经有 TraceIdFilter,它负责把追踪信息放入请求上下文。

第二步:Interceptor 读取 traceId 并做鉴权

package com.example.demo.interceptor;

import com.example.demo.filter.TraceIdFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        String traceId = (String) request.getAttribute(TraceIdFilter.TRACE_ID);
        String token = request.getHeader("Authorization");

        if (token == null || token.isBlank()) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("""
                {"code":401,"message":"未登录","traceId":"%s"}
                """.formatted(traceId));
            return false;
        }

        return true;
    }
}

这样做的好处是:

  • 无论请求是否进入 Controller,都已经有 traceId
  • 拦截器返回的鉴权失败响应也能带上统一链路标识
  • 日志、异常、审计都能串起来

这比把所有事情一股脑塞进一个 Filter 或一个 Interceptor 更清晰。


什么时候优先选 Filter

下面这些情况,优先用 Filter 基本不会错:

1. 需要在 Spring MVC 之前执行

比如你要做跨域、统一编码、最早期请求日志,这些都应该尽可能靠前。

2. 需要包装 HttpServletRequest / HttpServletResponse

例如缓存请求体、重复读取响应体、统一输出压缩、修改 Header。

3. 处理对象与具体业务无关

比如 traceId、底层审计、性能打点、网关前置规则。

4. 希望能力更通用

Filter 属于 Servlet 标准,不依赖 Spring MVC,迁移到别的 Servlet 应用依然成立。


什么时候优先选 Interceptor

下面这些情况,更适合 Interceptor:

1. 需要知道当前命中的是哪个接口

权限控制、注解识别、接口级审计,都依赖 handler。

2. 逻辑明显属于业务接口层

例如登录校验、租户识别、幂等校验、接口白名单。

3. 需要与 Spring MVC 行为保持一致

包括路径匹配、异常链、控制器生命周期。

4. 希望只对某些接口生效

addPathPatterns 和 excludePathPatterns 在业务场景下通常比 Filter 的 URL 模式更顺手。


常见误区与线上坑点

误区一:把所有鉴权都写进 Filter

这会导致两个问题:

  • 你拿不到明确的 Controller 方法信息
  • 很容易把业务规则和基础设施规则混在一起

基础接入控制可以在 Filter 做,比如非法来源直接拒绝。 但接口级权限、注解式权限校验,通常放在 Interceptor 更合适。


误区二:在 Interceptor 里做请求体读取

Interceptor 能拿到 HttpServletRequest,但它不适合承担底层请求包装职责。 如果你在这里直接读取输入流,很容易影响后续参数绑定,导致 Controller 拿不到请求体。

涉及请求体缓存、重复读取、响应体包装,应该在 Filter 层处理。


误区三:忘记异步请求的特殊性

如果 Controller 返回的是异步结果,比如 Callable、DeferredResult,请求线程和业务执行线程可能不是同一个。

这时要注意:

  • ThreadLocal 里的上下文可能丢失
  • postHandle 的触发时机和你想象的不一样
  • afterCompletion 才更适合做最终清理

如果你在拦截器里保存用户上下文到 ThreadLocal,一定要在 afterCompletion 中清理。


误区四:多个拦截器/过滤器顺序没设计

系统复杂以后,通常会同时存在:

  • 日志 Filter
  • 安全头 Filter
  • traceId Filter
  • 登录拦截器
  • 权限拦截器
  • 幂等拦截器

如果顺序不清晰,常见结果是:

  • 未登录日志没有 traceId
  • 幂等校验早于登录校验
  • 异常处理和日志落库缺字段

建议明确一套顺序策略:

Filter 顺序建议

  1. traceId
  2. 请求包装 / 请求日志
  3. CORS / 安全头
  4. 其他基础设施 Filter

Interceptor 顺序建议

  1. 登录校验
  2. 权限校验
  3. 幂等校验
  4. 审计增强

误区五:preHandle(false) 后什么都不写

如果你返回 false,Spring MVC 不会继续进入 Controller。 但这不等于客户端就自动得到一个规范响应。你必须自己明确设置:

  • HTTP 状态码
  • 响应头
  • 响应体

否则很容易出现前端收到空响应、乱码或者难以解析的错误。


Spring Boot 2.x 与 3.x 这里最容易踩的版本差异

如果你在搜索资料时看到很多旧代码,最常见的版本坑是 Servlet 包名变化。

Spring Boot 2.x

使用:

import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

Spring Boot 3.x

使用:

import jakarta.servlet.Filter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

如果你是 Boot 3 项目,却复制了 Boot 2 的 javax.servlet.* 示例代码,编译会直接失败。

这不是拦截器或过滤器原理的问题,而是 Jakarta EE 命名空间升级带来的版本差异。


一个实用的选择口诀

遇到需求时,可以直接用下面这组判断:

需求是“改请求、包响应、做底层链路”

选 Filter

需求是“看接口、做鉴权、按业务规则拦截”

选 Interceptor

两者都需要

组合使用,职责分层,不要互相替代


总结

Filter 和 Interceptor 不是谁取代谁的关系,而是位于不同层次的两个拦截点。

Filter 更底层,属于 Servlet 规范,适合处理请求对象本身和全局基础设施问题。 Interceptor 更贴近 Spring MVC,适合处理接口级别的业务治理问题。

在 Spring Boot 项目里,最稳妥的实践不是二选一,而是分工明确:

  • 用 Filter 处理链路追踪、请求包装、跨域、底层日志
  • 用 Interceptor 处理登录校验、权限控制、租户上下文、业务审计

真正写得好的项目,不是“哪里都能拦”,而是“每一层只做该做的事”。

正文到此结束
评论插件初始化中...
Loading...