原创

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

为什么 FilterInterceptor 容易被混淆

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

一句话先定性:

  • FilterServlet 规范层 的组件,工作在整个 Web 请求链更靠前的位置
  • InterceptorSpring 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 参数为什么重要

很多时候,InterceptorFilter 更适合做业务鉴权,不是因为它“更 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. 希望只对某些接口生效

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


常见误区与线上坑点

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

这会导致两个问题:

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

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


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

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

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


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

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

这时要注意:

  • 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

两者都需要

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


总结

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

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

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

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

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

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