Spring Boot过滤器与拦截器:核心机制、应用场景与最佳实践
1. 从一次线上故障说起为什么我们需要“门卫”和“安检员”那天下午监控系统突然告警某个核心接口的响应时间从平时的50毫秒飙升到了5秒以上紧接着错误率也开始攀升。登录服务器一看日志里充斥着大量来自同一个IP段的、请求路径毫无规律的垃圾请求。这些请求没有携带任何有效的业务参数纯粹是为了消耗服务器资源而来。我们的应用就像一个敞开大门、毫无防备的商场任何人都可以随意进出甚至在里面横冲直撞。这次事件让我深刻意识到在Web应用的世界里业务逻辑的健壮只是“内功”而一套有效的“门禁”和“安检”系统才是抵御外部混乱、保障内部秩序的第一道防线。在Java Web开发尤其是Spring Boot生态中这套系统主要由两个核心组件构成过滤器Filter和拦截器Interceptor。很多开发者包括曾经的我对它们的概念、区别以及应用场景都只有模糊的认识经常混用或错误配置导致安全漏洞、性能瓶颈甚至是功能异常。简单来说你可以把过滤器想象成小区或大楼的门卫。他的工作范围很广所有进出的人HTTP请求和响应都必须经过他。他可以检查你的证件请求头、测量你的体温参数校验、甚至拒绝你携带危险品入内修改请求/响应内容。他的权力很大发生在最底层由Servlet容器如Tomcat直接管理。而拦截器则更像是公司前台或特定部门的安检员。只有在访客请求已经进入大楼Web应用并且明确要去某个办公室Controller时才会遇到他。他的职责更聚焦比如检查你的预约单Session/Token、记录你的到访信息日志、或者根据公司规定引导你去不同的会议室预处理数据。他的工作发生在Spring MVC的框架流程之内。理解这两位“安全官”的职责边界、工作机制以及如何协同是构建一个稳固、高效、安全的后端服务的基石。接下来我们就深入探索它们的奥秘并手把手教你如何用好它们来“保卫你的应用”。2. 庖丁解牛过滤器与拦截器的核心机制对比光有比喻还不够我们需要从技术实现层面彻底拆解它们这决定了你在什么场景下该用谁。这个选择错误往往会埋下难以调试的坑。2.1 出身与管辖范围容器级 vs 框架级这是最根本的区别决定了它们的加载时机和作用域。过滤器Filter 由Servlet 规范定义。这意味着它是Java EE现Jakarta EE标准的一部分不依赖于任何特定的Web框架。你的Spring Boot应用最终会打包成一个WAR包或者内嵌一个Servlet容器如Tomcat、Jetty来运行Filter就是由这个Servlet容器来管理和执行的。因为它作用在Servlet层面所以它能过滤一切请求包括静态资源如/css/style.css、错误页面/error以及你定义的API接口。只要请求进了Servlet容器就会经过Filter链。拦截器Interceptor 是Spring MVC 框架的一部分。它由Spring框架定义和管理。因此它的生效范围仅限于Spring MVC的DispatcherServlet处理流程之内。一个请求必须先被DispatcherServlet接收通常映射为/然后才会进入拦截器链。这意味着对于直接由容器处理的请求比如配置了spring.mvc.static-path-pattern的静态资源或者被其他Servlet处理的请求拦截器是无效的。实操心得如果你需要对所有流量包括健康检查端点/actuator/health、Swagger文档/v3/api-docs进行统一操作比如全局请求耗时统计或基础安全防护必须使用过滤器。如果你只关心进入你业务Controller的请求那么拦截器更合适、也更轻量。2.2 生命周期与执行时机包裹一切 vs 嵌入流程它们的执行节点在请求-响应周期中的位置截然不同下图清晰地展示了这一点客户端请求 ↓ Servlet容器接收 ↓ ------ 【过滤器Filter开始工作】 ------ ↓ FilterChain.doFilter() ↓ ------ 【进入Spring MVC领域】 ------ ↓ DispatcherServlet ↓ ------ 【拦截器InterceptorpreHandle】 ------ ↓ HandlerMapping (找到对应Controller方法) ↓ HandlerAdapter (执行参数绑定等) ↓ ------ 【执行你的Controller方法】 ------ ↓ ------ 【拦截器InterceptorpostHandle】 ------ ↓ 视图渲染如果存在 ↓ ------ 【拦截器InterceptorafterCompletion】 ------ ↓ ------ 【过滤器Filter后续处理】 ------ ↓ 返回响应给客户端过滤器的生命周期与Servlet容器同步。它在应用启动时由容器初始化调用init方法在请求到达时执行doFilter方法在应用关闭时销毁调用destroy方法。关键点在于doFilter方法里有一个FilterChain参数你必须调用chain.doFilter(request, response)才能将请求传递给链中的下一个过滤器最终到达Servlet。你可以在调用chain.doFilter之前和之后分别编写逻辑相当于包裹住了整个后续处理过程。拦截器的生命周期由Spring MVC管理。它提供了三个精确的切入点preHandle(HttpServletRequest request, HttpServletResponse response, Object handler): 在Controller方法执行之前调用。返回值是boolean如果返回false则中断流程后续的拦截器和Controller都不会执行。这里适合做权限校验、日志记录。postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView): 在Controller方法执行之后但视图渲染之前调用对于REST API视图渲染通常不适用。此时可以修改ModelAndView数据。注意如果Controller方法内部或preHandle中发生了异常此方法不会被调用。afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex): 在整个请求结束之后调用无论是否有异常发生。这里适合进行资源清理、性能监控统计计算总耗时等收尾工作。2.3 获取信息的差异粗糙与精细由于执行时机不同它们能获取到的上下文信息也不同。在过滤器的doFilter中你拿到的是最原始的ServletRequest和ServletResponse。此时Spring MVC的魔法还没有开始你不知道这个请求最终会被哪个Controller的哪个方法处理。你只能通过请求路径request.getRequestURI()等粗略信息进行判断。在拦截器的preHandle等方法中你多了一个关键的Object handler参数。这个handler就是Spring MVC映射到的具体处理单元通常是HandlerMethod类型你可以通过它获取到目标Controller类、方法、以及方法上的注解如RequestMapping,PreAuthorize等丰富的元信息。这为实现基于注解的精细权限控制提供了可能。// 在拦截器中可以这样获取注解信息 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; // 获取方法上的特定注解 MyPermission anno handlerMethod.getMethodAnnotation(MyPermission.class); if (anno ! null) { // 进行基于注解的权限逻辑 } }3. 实战配置在Spring Boot中注册你的守卫理解了原理我们来看看在Spring Boot中如何具体实现和配置它们。Spring Boot的自动配置让这一切变得简单但细节决定成败。3.1 创建并注册一个过滤器有两种主流方式使用Component注解或通过FilterRegistrationBean。强烈推荐后者因为它能提供更精确的控制。方式一使用ComponentOrder(简单但控制力弱)Component Order(1) // 定义过滤器的执行顺序数字越小优先级越高 public class LoggingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long startTime System.currentTimeMillis(); HttpServletRequest req (HttpServletRequest) request; // 在chain.doFilter之前执行记录请求开始 System.out.println(Logging Filter - Request URI: req.getRequestURI() | Start Time: startTime); try { // 关键传递请求和响应给下一个过滤器或Servlet chain.doFilter(request, response); } finally { // 在chain.doFilter之后执行记录请求结束和耗时 long duration System.currentTimeMillis() - startTime; System.out.println(Logging Filter - Request URI: req.getRequestURI() | Duration: duration ms); } } Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化逻辑通常为空 } Override public void destroy() { // 销毁逻辑通常为空 } }这种方式虽然简单但你不能控制过滤器对哪些URL模式生效它会默认应用于/*所有请求。方式二使用FilterRegistrationBean(推荐功能强大)Configuration public class FilterConfig { Bean public FilterRegistrationBeanLoggingFilter loggingFilter() { FilterRegistrationBeanLoggingFilter registrationBean new FilterRegistrationBean(); registrationBean.setFilter(new LoggingFilter()); // 明确指定URL模式只对/api/*的请求生效 registrationBean.addUrlPatterns(/api/*); // 设置顺序 registrationBean.setOrder(1); // 给过滤器起个名字方便调试 registrationBean.setName(loggingFilter); return registrationBean; } Bean public FilterRegistrationBeanXssFilter xssFilter() { FilterRegistrationBeanXssFilter registrationBean new FilterRegistrationBean(); registrationBean.setFilter(new XssFilter()); // 假设有一个防XSS的过滤器 registrationBean.addUrlPatterns(/*); // 对所有请求生效 registrationBean.setOrder(2); // 在日志过滤器之后执行 registrationBean.setName(xssFilter); return registrationBean; } }通过FilterRegistrationBean你可以精确控制过滤器的URL匹配模式、顺序、初始化参数甚至通过setDispatcherTypes来控制过滤器在请求REQUEST、转发FORWARD、包含INCLUDE、错误ERROR、异步ASYNC等不同分发类型时是否生效。这是生产环境的首选。3.2 创建并配置一个拦截器拦截器的配置更“Spring化”需要实现HandlerInterceptor接口并通过WebMvcConfigurer进行注册。第一步实现拦截器Component // 将其声明为Spring组件方便注入其他Bean public class AuthInterceptor implements HandlerInterceptor { Autowired private TokenService tokenService; // 可以注入其他Spring Bean Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 静态资源直接放行 if (!(handler instanceof HandlerMethod)) { return true; } // 2. 检查方法或类上是否有Anonymous注解自定义的免鉴权注解 HandlerMethod handlerMethod (HandlerMethod) handler; if (handlerMethod.hasMethodAnnotation(Anonymous.class) || handlerMethod.getBeanType().isAnnotationPresent(Anonymous.class)) { return true; } // 3. 进行Token验证 String token request.getHeader(Authorization); if (token null || !tokenService.validateToken(token)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write({\code\: 401, \msg\: \Unauthorized\}); return false; // 中断请求 } // 4. 将用户信息存入请求上下文供Controller使用 UserInfo user tokenService.getUserFromToken(token); request.setAttribute(CURRENT_USER, user); return true; // 放行 } Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 可以对ModelAndView进行修改常用于传统MVC项目 // 对于纯API项目这里通常不需要操作 } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求完成后的清理工作例如移除ThreadLocal变量防止内存泄漏 UserContextHolder.clear(); // 假设有一个存放用户信息的ThreadLocal if (ex ! null) { // 可以在此处记录异常日志但注意异常已经被ControllerAdvice处理过了 log.error(Request completed with exception: {}, request.getRequestURI(), ex); } } }第二步注册拦截器到Spring MVCConfiguration public class WebConfig implements WebMvcConfigurer { Autowired private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) // 指定拦截的路径模式 .excludePathPatterns(/api/auth/login, /api/auth/register, /error) // 排除的路径 .order(0); // 定义拦截器执行顺序数字越小越先执行 // 可以继续添加更多拦截器 // registry.addInterceptor(new LogInterceptor()).addPathPatterns(/**).order(1); } }这里的关键是addPathPatterns和excludePathPatterns它们提供了灵活的URL匹配控制。order方法用于管理多个拦截器的执行顺序。4. 经典应用场景剖析何时用Filter何时用Interceptor理论结合实践我们通过几个典型场景来固化认知避免误用。4.1 场景一全局请求日志与耗时统计需求记录每一个进入应用的请求的路径、方法、客户端IP和耗时。选择过滤器Filter。原因日志和耗时统计需要覆盖最广的范围包括静态资源、健康检查端点、未映射的404请求等。过滤器工作在Servlet层面能捕获所有这些流量。在doFilter方法开始前记录开始时间在chain.doFilter后的finally块中计算耗时并记录非常自然。拦截器为何不合适拦截器无法处理未经DispatcherServlet路由的请求如一些静态资源、直接由容器处理的错误页面。此外在afterCompletion中虽然可以计算总耗时但无法覆盖过滤器阶段的耗时。4.2 场景二接口权限认证与授权需求验证用户Token并根据Controller方法上的注解判断用户是否有权限访问。选择拦截器Interceptor。原因权限校验通常需要知道请求最终要执行哪个Controller方法以便读取方法上的权限注解如PreAuthorize(“hasRole(‘ADMIN’)”)或自定义的RequiresPermission。这只有在Spring MVC将请求映射到HandlerMethod之后才能实现这正是拦截器preHandle方法提供的handler参数。此外权限校验失败后我们通常需要返回一个结构化的JSON错误信息这在拦截器中操作HttpServletResponse也很方便。过滤器为何不合适在过滤器中我们无法直接、方便地获取到方法级别的注解信息需要自己解析请求路径并与权限配置映射表进行匹配这增加了复杂度和维护成本且容易出错。4.3 场景三防XSS与SQL注入攻击需求对请求参数进行清洗过滤潜在的恶意脚本或SQL关键字。选择过滤器Filter。原因安全防护越早进行越好。在请求进入任何业务逻辑包括Spring MVC框架之前就将其中的危险内容过滤或转义是最彻底的防御方式。我们可以编写一个XSS过滤器对HttpServletRequest进行包装重写getParameter,getHeader等方法在其中对返回值进行HTML转义。这样后续所有从request对象获取参数的代码拿到的都是安全的字符串。public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 使用自定义的包装类 XssHttpServletRequestWrapper wrappedRequest new XssHttpServletRequestWrapper((HttpServletRequest) request); chain.doFilter(wrappedRequest, response); } } // XssHttpServletRequestWrapper 需要继承 HttpServletRequestWrapper 并重写相关方法4.4 场景四全局响应体包装与格式化需求对所有Controller返回的JSON数据统一包装成{“code”: 200, “msg”: “success”, “data”: ...}的格式。选择两者结合但各有侧重。这是一个高级场景单独使用过滤器或拦截器都有缺陷。仅用拦截器在postHandle中操作ModelAndView对API不友好且无法处理ResponseBody注解的方法。虽然可以通过实现ResponseBodyAdvice接口来包装响应体但这属于另一种机制AOP范畴。仅用过滤器在chain.doFilter之后虽然能拿到ServletResponse但响应流可能已经被写入并关闭了直接操作风险高。更优雅的方案使用ControllerAdviceResponseBodyAdvice接口这是Spring官方推荐的、对返回体进行全局处理的方式。它可以完美地拦截所有带有ResponseBody注解或RestController的控制器方法的返回值并进行包装。使用过滤器处理异常情况对于未被Controller捕获、直接由过滤器链抛出的异常例如在过滤器中因校验失败直接返回响应过滤器需要负责处理响应格式。此时可以在过滤器中设置一个自定义的HttpServletResponseWrapper捕获写入流的内容但这比较复杂。踩坑实录曾经尝试在过滤器的chain.doFilter之后直接通过response.getWriter()写入包装格式结果导致Controller返回的原始数据也被写入响应体变成了混乱的拼接数据。根本原因在于响应输出流的管理权问题。最终我们采用了ResponseBodyAdvice来处理正常流程的包装并配合一个全局异常处理器ControllerAdviceExceptionHandler来保证异常情况下的响应格式也是一致的。5. 高阶话题与性能陷阱当你熟练运用过滤器和拦截器后一些高阶问题和潜在的性能陷阱就需要纳入考量了。5.1 执行顺序的迷宫当多个守卫同时存在当你有多个过滤器和多个拦截器时它们的执行顺序遵循特定规则理解错误会导致逻辑混乱。过滤器顺序由FilterRegistrationBean中的order值决定值越小优先级越高。在doFilter方法中chain.doFilter()之前的逻辑是正序执行之后的逻辑是逆序执行。 假设有FilterA(order1)和FilterB(order2)请求进入 - FilterA.pre逻辑 - FilterB.pre逻辑 - Servlet处理 - FilterB.post逻辑 - FilterA.post逻辑 - 响应返回拦截器顺序由WebMvcConfigurer.addInterceptors中注册时的order值决定值越小优先级越高。preHandle方法是正序执行postHandle和afterCompletion是逆序执行。 假设有InterceptorA(order0)和InterceptorB(order1)preHandle: A - B postHandle: B - A afterCompletion: B - A (在视图渲染后且一定执行)过滤器与拦截器的总体顺序过滤器链永远在拦截器链的外层。请求 - Filter Chain - DispatcherServlet - Interceptor Chain - Controller 响应 - Filter Chain - DispatcherServlet - Interceptor Chain - Controller5.2 异步请求下的特殊行为现代应用常用异步处理如DeferredResult,Callable, WebFlux。在异步场景下拦截器的postHandle方法会立即执行在异步任务开始之前主线程就返回了而不是在异步任务完成之后。而afterCompletion方法会在异步任务处理完成后由异步线程调用。这意味着如果你在postHandle中依赖处理结果在异步场景下会出错。解决方案对于异步请求如果需要在线程处理结束后进行操作应使用AsyncHandlerInterceptor接口它是HandlerInterceptor的子接口它提供了一个额外的afterConcurrentHandlingStarted方法。更常见的做法是将需要在请求完成后执行的逻辑如日志记录、资源清理放在afterCompletion中或者使用Spring的RequestContextListener、EventListener等监听器机制。5.3 性能考量与常见陷阱避免在过滤器和拦截器中执行重量级操作它们对每个请求都会执行。频繁的数据库查询、远程RPC调用会迅速拖慢整个应用。对于权限校验应使用高效的Token验证机制如JWT本地验证或结合缓存。注意线程安全问题过滤器和拦截器默认是单例的所有线程共享同一个实例。因此绝对不要在它们的成员变量中存储与特定请求相关的状态如用户信息。应该使用ThreadLocal或直接将信息存入HttpServletRequest属性中。谨慎处理请求/响应流在过滤器中读取ServletRequest的输入流getInputStream()或写入ServletResponse的输出流后后续的过滤器或Controller可能就无法再次读取或写入了因为流可能已被消费。如果必须读取请求体考虑使用ContentCachingRequestWrapper进行包装但这会带来内存开销。精确配置路径模式使用/*和/**要小心。/*是Servlet规范中的路径映射通常匹配单层路径。/**是Spring的Ant风格路径匹配可以匹配多层路径。在过滤器中使用/*可能无法匹配到/api/v1/user这样的路径而在拦截器中应使用/**。最好的实践是根据实际需求使用最精确的路径模式并利用excludePathPatterns排除不需要的路径减少不必要的拦截开销。6. 从“能用”到“用好”最佳实践与架构思考掌握了基本用法和避开了常见陷阱后如何让过滤器和拦截器成为你应用架构中的得力助手而不仅仅是功能堆砌这里分享一些从实际项目中总结出的最佳实践。6.1 职责分离与单一职责一个过滤器或拦截器只应做好一件事。不要编写一个“全能”的GlobalSecurityFilter里面既做XSS过滤、又做SQL注入防护、还做权限校验和日志记录。这会导致代码臃肿难以维护一个类上千行逻辑纠缠。调试困难问题出现时难以定位是哪个环节的逻辑出错。执行顺序耦合内部逻辑如果存在依赖顺序难以调整。正确做法是拆分成多个独立的组件LoggingFilter: 只负责请求/响应日志和耗时统计。XssFilter: 只负责请求参数的HTML转义。AuthInterceptor: 只负责Token验证和权限注解解析。RateLimitInterceptor: 只负责接口限流。然后通过order属性精确控制它们的执行顺序。例如通常的执行顺序应该是日志 - 全局安全清洗XSS - 限流 - 认证 - 授权 - 业务Controller。6.2 可配置化与动态生效在生产环境中我们可能需要动态调整某些规则比如临时关闭某个接口的限流、调整黑白名单IP等。硬编码在过滤器/拦截器类中显然不可取。解决方案利用配置中心将规则如IP白名单、限流阈值、需要放行的路径配置在Apollo、Nacos等配置中心。在过滤器/拦截器的逻辑中读取这些配置。监听配置变更实现EnvironmentAware或使用ConfigurationProperties并监听配置变更事件动态更新内存中的规则缓存避免每次请求都去远程读取配置。使用Spring Profiles为不同的环境dev, test, prod定义不同的配置控制某些过滤器是否被加载。可以通过条件化BeanConditionalOnProperty来实现。Configuration public class FilterConfig { Bean ConditionalOnProperty(name “security.xss.enabled”, havingValue “true”, matchIfMissing true) public FilterRegistrationBeanXssFilter xssFilter() { // ... 仅当配置了security.xss.enabledtrue或未配置时才注册此过滤器 } }6.3 与Spring Security的协同Spring Security本身就是一个基于过滤器链的庞大安全框架。当你引入Spring Security后它会自动注册一个名为FilterChainProxy的过滤器这个过滤器内部包含了一条完整的安全过滤器链如UsernamePasswordAuthenticationFilter,BasicAuthenticationFilter,FilterSecurityInterceptor等。关键原则你的自定义过滤器通常需要插入到Spring Security过滤器链的特定位置之前或之后。例如一个用于解析JWT Token并设置认证信息的过滤器就需要在UsernamePasswordAuthenticationFilter之前执行。如何集成创建继承自OncePerRequestFilter的过滤器这是Spring提供的一个便捷抽象确保在一次请求中只执行一次避免在转发forward时被重复执行。通过HttpSecurity配置将其添加到安全链中Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 在某个过滤器前添加 // .addFilterAfter(new CustomFilter(), BasicAuthenticationFilter.class) // 在某个过滤器后添加 .authorizeRequests() // ... 其他配置 } }绝对不要同时使用Component和HttpSecurity.addFilterBefore来注册同一个过滤器实例否则会导致重复执行。通常在Security配置中直接new一个实例或通过Autowired注入一个Bean都是可以的但要确保单例。6.4 全面的监控与测试对于核心的全局组件必须建立完善的监控和测试体系。监控在过滤器和拦截器的关键位置开始、结束、异常捕获处记录Metrics指标如请求量、平均耗时、错误次数。集成到Micrometer并输出到PrometheusGrafana。为重要的业务拦截器如权限校验设置独立的告警。例如权限校验失败率在5分钟内超过1%就触发告警这可能意味着Token服务异常或遭受了撞库攻击。测试单元测试针对过滤器/拦截器本身的业务逻辑进行测试。MockHttpServletRequest,HttpServletResponse,FilterChain,HandlerMethod等对象验证在各种输入下逻辑是否正确响应是否符合预期。集成测试使用SpringBootTest和MockMvc启动一个接近真实的环境测试整个过滤器/拦截器链与Controller的协作是否正常。验证路径匹配、顺序、异常处理等。SpringBootTest AutoConfigureMockMvc class AuthInterceptorTest { Autowired private MockMvc mockMvc; Test void testApiWithoutToken_ShouldReturn401() throws Exception { mockMvc.perform(get(“/api/protected”)) .andExpect(status().isUnauthorized()); } }压力测试使用JMeter或Gatling模拟高并发场景验证添加了全局过滤器/拦截器后对应用QPS和响应时间的影响是否在可接受范围内。特别要关注那些进行了复杂操作如加解密、远程调用的组件。通过遵循这些最佳实践过滤器和拦截器就能从简单的代码工具演进为支撑应用安全性、可观测性和可维护性的核心架构组件。它们的正确使用是区分一个“能跑”的应用和一个“健壮、优雅、易维护”的应用的重要标志之一。