news 2026/9/24 18:47:30

Servlet过滤器实战:统一编码、登录认证与XSS防护的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Servlet过滤器实战:统一编码、登录认证与XSS防护的完整指南

前阵子同事被一个线上问题折腾了一下午:用户提交中文昵称之后,数据库里存进去的全是问号,页面传回来又变成乱码。排查了半天,发现每个Servlet都各自写了一套编码转换逻辑,而他新写的接口偏偏漏掉了。我顺手在项目里加了一个统一字符编码过滤器,五分钟解决了全站所有的乱码问题。

这就是Servlet过滤器最典型的价值。说实话,Servlet过滤器算是Java Web开发里最被低估的技术之一——它不花哨,但几乎所有横切关注点(编码、登录校验、日志、安全过滤、响应压缩)都依赖它来实现。不管你是刚学Servlet的新手,还是已经转战Spring Boot的老手,把过滤器机制吃透,都能少踩很多坑。这篇就围绕“Servlet编写过滤器”这件事,把我这些年实际用过的写法、踩过的坑、排查过的诡异问题一次性整理出来。

1. 过滤器到底是做什么的:一次请求的完整旅程

1.1 过滤器在请求处理链路中的位置

想象一个HTTP请求从浏览器发出后,经过了哪些环节。请求到达Tomcat,容器创建ServletRequest和ServletResponse对象,然后不是直接丢给Servlet处理,而是先经过一层又一层过滤器。这些过滤器围着目标Servlet形成一圈“洋葱皮”,请求要穿过层层外皮才能到达内核,响应返回时又要逆着路径再穿一次。

客户端 → 过滤器F1 → 过滤器F2 → 过滤器F3 → Servlet → 业务处理 → 视图渲染 ↑ ↑ ↑ 响应原路返回,逐层过滤器的后置逻辑执行

每个过滤器都可以在请求前做一些处理(比如校验用户是否登录、设置请求编码、记录开始时间),也可以决定是否放行(调用FilterChain.doFilter),甚至可以直接终止请求(比如未登录直接重定向到登录页)。这就是“过滤”二字的本质——在到达Servlet之前,把不符合条件的请求拦下来。

1.2 过滤器解决的核心痛点

没有过滤器的时候,那些通用逻辑是怎么处理的?最简单粗暴的写法是每个Servlet里都复制粘贴一遍:设置编码、判断session、写日志。一旦项目里有一百个Servlet,修改登录校验逻辑就要动一百处。更麻烦的是,这还容易漏——漏掉一个,线上就出乱码,就是开头同事遇到的那种问题。

过滤器把这类“横切逻辑”抽离出来,形成统一的处理层,这是它能避免的问题的核心:一是消除了重复代码,二是让业务Servlet只关心自己的业务处理,三是修改公共逻辑时只动一个过滤器就够了。过滤器这个机制本质上就是Servlet规范里内置的AOP(面向切面编程)——把公共逻辑织入到请求处理链路的特定位置。

1.3 有哪些东西适合放进过滤器

实际项目里,过滤器典型的应用场景包括:

  • 统一编码处理:设置request和response的字符集,解决中文乱码
  • 登录认证与权限校验:检查session或token,未登录的请求重定向到登录页
  • 日志记录与性能监控:记录请求路径、耗时、客户端IP
  • 安全防护:XSS攻击防护、SQL注入过滤、上传文件类型校验
  • 敏感信息脱敏与数据压缩
  • 黑白名单过滤:比如配合布隆过滤器快速判断请求IP是否命中黑名单

要注意的是,过滤器适合做的是“请求层面的统一处理”,而不是某个具体业务逻辑。这点在后面的过滤器与拦截器对比时还会详细展开。

2. Filter API与生命周期:写过滤器前必须吃透的底层细节

2.1 Filter接口的三个核心方法

Filter接口一共就三个方法,代码量不大,但每个方法都得搞清楚什么时候执行。

package javax.servlet; public interface Filter { default void init(FilterConfig filterConfig) throws ServletException {} void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; default void destroy() {} }

init方法在过滤器实例被创建后执行,只执行一次。这里适合做初始化工作,比如读取配置参数、初始化Redis连接、加载布隆过滤器数据。需要注意,这里的init是“懒加载”的——并不是应用启动时所有过滤器都会初始化,而是第一次有请求匹配到这个过滤器时才会初始化。在Servlet 3.0之后可以通过web.xml的metadata-complete属性和load-on-startup配置来调整,但通常不用太纠结。

doFilter是核心方法,每个匹配请求都会执行。这里做具体的过滤逻辑,最重要的是记得调用chain.doFilter(request, response)放行,否则请求就会被卡死在这里,永远不会到达Servlet。

destroy方法在过滤器被销毁时执行,适合释放资源。这个一般很少用到,但如果你在init里开了连接池,一定要在这里关掉。

2.2 生命周期与FilterConfig

过滤器的生命周期由容器管理:启动时创建实例(懒加载),请求时反复调用doFilter,服务停止时调用destroy。这里有个容易踩坑的点:Filter是单例多线程的,同一个过滤器实例会被多个请求线程并发调用,所以doFilter里写的代码要注意线程安全,不能在里面声明全局可变状态去存请求数据。

FilterConfig用来获取初始化参数,比如在web.xml里配的init-param。读参数的方法很简单:

String encoding = filterConfig.getInitParameter("encoding");

这里再提一个版本差异问题。Servlet 4.0及之前的规范使用javax.servlet包名,而从Servlet 5.0开始改成了jakarta.servlet包名。如果你用的是Tomcat 10+,依赖和import都要写jakarta.servlet;如果还是Tomcat 9及以下,就用javax.servlet。这个坑很多人踩过——代码里明明没错,启动却报NoClassDefFoundError,十有八九是包名和容器版本对不上。

2.3 线程安全与性能注意事项

刚才说了过滤器是单例的,所以在doFilter里不要做以下这些事情:把请求对象存到成员变量里、声明一个实例级的List去收集请求参数、用实例级变量保存耗时统计(除非有同步机制)。但也不必过度恐慌,只要不在doFilter里写实例变量,过滤器天然就是线程安全的。

性能方面,过滤器的执行在请求处理链路中是前置环节,如果有耗时的操作(比如查数据库、调外部API),会拖慢整个请求的处理时间。所以在过滤器里做缓存加载、黑名单查询这类操作时,尽量用内存缓存或布隆过滤器这类高性能数据结构,别每次请求都穿透到数据库。

3. 从零搭建VSCode的Servlet开发环境:JDK、Tomcat和Maven一次配好

3.1 环境准备:JDK和Tomcat

写这篇文章前看到很多人问“VSCode编写Servlet代码需要怎么配置环境”,这里就完整走一遍。Servlet项目本质上是Java Web项目,运行在Servlet容器(最常见的是Tomcat)里,所以核心是三件套:JDK、Tomcat、构建工具。

JDK建议装JDK 8(对应Tomcat 9)或JDK 11/17(对应Tomcat 10或11)。装完后在系统环境变量里配置JAVA_HOME,命令行执行java -version能正常输出版本号就行。Tomcat去官网下载对应版本的zip包,解压到一个无中文无空格的路径,比如D:\apache-tomcat-9.0.98,然后配置CATALINA_HOME指向这个目录。

如果是Tomcat 10及以上的版本,注意Servlet依赖要用jakarta.servlet-api,这一点后面pom.xml里会体现。

3.2 VSCode插件安装与Maven工程初始化

VSCode写Servlet代码,插件装好体验完全不输IDEA。必装的是“Extension Pack for Java”(包含了语言支持、调试器、Maven支持等一系列插件),再装一个“Tomcat for Java”用于在VSCode里管理Tomcat服务器。

然后初始化一个Maven Web项目。VSCode里新建项目选Maven Archetype,用maven-archetype-webapp这个原型模板生成标准的Web项目骨架。命令行方式也可以:

mvn archetype:generate \ -DgroupId=com.example \ -DartifactId=servlet-filter-demo \ -DarchetypeArtifactId=maven-archetype-webapp \ -DinteractiveMode=false

3.3 引入Servlet依赖和Tomcat部署

生成的项目里打开pom.xml,加上Servlet依赖。这里有个关键点:scope要写成provided,因为Servlet API由Tomcat提供,如果打成war包还把Servlet API打包进去,会和容器自带的类冲突。

<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- Tomcat 10及以上改用 --> <!-- <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>5.0.0</version> <scope>provided</scope> </dependency> --> </dependencies>

部署到Tomcat有两种方式:在VSCode里用“Tomcat for Java”插件直接右键Add Deployment,把war包添加进Tomcat服务器;或者执行mvn package打war包,把target目录下生成的war复制到Tomcat的webapps目录里,启动Tomcat即可自动解压部署。

3.4 启动验证

启动Tomcat后,访问http://localhost:8080/项目名/,能看到index.jsp页面就说明环境通了。到这里,Servlet项目的开发环境就配好了。很多人第一步就卡在环境配置上,其实这套流程走通一次之后,后面创建项目都是重复动作,熟练了五分钟就能搞定。

4. 动手实现第一个过滤器:注解和web.xml两种注册方式

4.1 写一个最简过滤器

不管用哪种方式注册,过滤器的核心类是一样的。下面是一个最简过滤器,会打印请求进入和响应返回的日志:

package com.example.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter(urlPatterns = "/*", filterName = "helloFilter") public class HelloFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("HelloFilter 初始化"); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("过滤器前:拦截到请求"); // 放行,让请求继续向下传递 chain.doFilter(request, response); System.out.println("过滤器后:响应正在返回客户端"); } @Override public void destroy() { System.out.println("HelloFilter 销毁"); } }

这个过滤器的执行顺序能看得很清楚:请求进来先打“过滤器前”,然后进入Servlet执行业务,Servlet返回后再打“过滤器后”。这就是过滤器双向处理的核心特征。

4.2 @WebFilter注解方式

上面的代码里的@WebFilter就是Servlet 3.0提供的注解注册方式。urlPatterns = "/*"表示拦截所有请求。注解方式的好处是无需额外配置文件,代码内聚,非常适合新项目。

但要注意一个问题:如果项目使用Spring Boot,通过@WebFilter注册的过滤器,Spring Boot不会自动扫描到,需要加上@ServletComponentScan注解才能被发现。这个在Spring Boot集成的时很关键,后面会单独讲。

4.3 web.xml方式与顺序控制

老项目或者在Servlet 3.0之前只能用web.xml声明过滤器。web.xml里做两件事:定义过滤器(filter标签)和配置映射(filter-mapping标签)。

<filter> <filter-name>helloFilter</filter-name> <filter-class>com.example.filter.HelloFilter</filter-class> <init-param> <param-name>name</param-name> <param-value>hello</param-value> </init-param> </filter> <filter-mapping> <filter-name>helloFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

在web.xml方式中,多个过滤器的执行顺序是按照filter-mapping的声明顺序执行的,声明在前面的先执行。这给了开发者精确控制过滤器顺序的能力。

注解方式则没有严格规范的顺序保证(不同容器实现不同),所以当多个过滤器之间有顺序依赖时,要么用web.xml,要么在Spring Boot里用FilterRegistrationBean的setOrder方法。

4.4 url-pattern匹配规则详解

url-pattern是过滤器配置里最容易写错的地方,它的匹配规则比较特殊:

表格示例:

写法匹配范围示例
/*匹配所有URL,最常用拦截所有请求
/api/*匹配/api路径下的所有请求/api/user、/api/order/list都匹配
*.do匹配以.do结尾的请求/user/add.do匹配
/login精确匹配某个路径只有/login这一个地址匹配
/只匹配应用根路径一般不会这么用

注意“/”和“/”区别很大,“/”匹配一切请求,包括访问静态资源、jsp页面;而“/”只匹配根路径本身。另外“/.do”这种写法是无效的,“/”和“”不能混用。实际项目里,统一编码、日志这类全局过滤器用“/”就行,登录认证过滤器通常配置成“/user/”、“/admin/*”等需要保护的路径。

5. 过滤器与过滤链:多个过滤器是如何串联起来的

5.1 FilterChain的调用机制

FilterChain是过滤器链的抽象,它维护了一个待执行的过滤器列表。当第一个过滤器调用chain.doFilter时,容器会从过滤链中取出下一个匹配的过滤器执行;直到过滤链上所有过滤器都执行完,最后一个chain.doFilter才会真正调用目标Servlet。

这个机制决定了过滤器必须通过chain.doFilter主动放行,请求才能继续向下传递。如果某个过滤器没有调用chain.doFilter,也没有返回响应或重定向,请求就会悬在那里,浏览器一直转圈,这是初学者最容易犯的错误。

5.2 洋葱模型:请求与响应的双向处理

由于过滤器在请求阶段和响应阶段都有机会执行代码,多个过滤器叠加起来就形成了经典的“洋葱模型”:

// 过滤器F1 chain.doFilter(request, response); // 过滤器F2 chain.doFilter(request, response); // Servlet

请求时按F1 → F2 → Servlet顺序向内穿透,响应返回时按Servlet → F2 → F1顺序向外穿出。这意味着F1的前置逻辑会在F2之前执行,但F1的后置逻辑会在F2之后执行。所以如果你要给请求做加密,过滤器要加在响应阶段(后置逻辑);要给请求做耗时统计,就前后都包上,用开始时间和结束时间做差。

5.3 锁定过滤器执行顺序的实战方法

日常项目中如果一个请求要经过多个过滤器,顺序就特别重要。比如先做编码处理,再做XSS过滤,最后做登录认证,这个顺序要是乱了,很可能出现编码没生效或者XSS过滤规则被绕过的安全隐患。

在web.xml中,过滤器执行顺序清晰可见:按filter-mapping的声明顺序从上到下执行。在Spring Boot中,用FilterRegistrationBean注册并设置order值,数字越小优先级越高:

@Configuration public class FilterConfig { @Bean public FilterRegistrationBean<EncodingFilter> encodingFilter() { FilterRegistrationBean<EncodingFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new EncodingFilter()); registrationBean.addUrlPatterns("/*"); registrationBean.setOrder(1); return registrationBean; } @Bean public FilterRegistrationBean<AuthFilter> authFilter() { FilterRegistrationBean<AuthFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new AuthFilter()); registrationBean.addUrlPatterns("/user/*"); registrationBean.setOrder(2); return registrationBean; } }

这个方案的关键在于,编码过滤器优先级最高(order=1),确保任何请求在进入后续业务逻辑前字符集都已经正确;安全过滤器排第二;登录认证最后。逻辑层次清楚,排查问题也直观。

6. 实用过滤器六种写法:直接在项目里抄作业

6.1 统一字符编码过滤器

这是最基础、也最刚需的过滤器——没有它,中文乱码问题能折磨人一整天。它的原理是在容器解析请求参数之前,先设置request的编码格式;在返回响应前设置response的编码和ContentType。

package com.example.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter(urlPatterns = "/*") public class EncodingFilter implements Filter { private String encoding = "UTF-8"; @Override public void init(FilterConfig filterConfig) { String encodingParam = filterConfig.getInitParameter("encoding"); if (encodingParam != null && !encodingParam.isEmpty()) { encoding = encodingParam; } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(encoding); response.setCharacterEncoding(encoding); response.setContentType("text/html;charset=" + encoding); chain.doFilter(request, response); } }

注意request.setCharacterEncoding必须在读取任何请求参数之前调用才有效。post请求的body在第一次getParameter时才会被解析,所以过滤器里设置编码是来得及的。但如果是Tomcat 8之前的版本,get请求的URL参数默认按ISO-8859-1解析,过滤器也救不了,需要修改server.xml的URIEncoding,不过Tomcat 8之后已经默认UTF-8,不用太担心。

6.2 登录认证过滤器

登录认证过滤器是业务系统里最常见的安全过滤器。核心逻辑是:检查当前请求是否带有有效的登录凭证(Session里的用户对象,或Header里的Token),没有就重定向到登录页,有就放行。

package com.example.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebFilter(urlPatterns = {"/user/*", "/order/*", "/admin/*"}) public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 放行登录接口本身,避免死循环 String uri = req.getRequestURI(); if (uri.endsWith("/login") || uri.endsWith("/login.html")) { chain.doFilter(request, response); return; } HttpSession session = req.getSession(false); Object loginUser = (session == null) ? null : session.getAttribute("loginUser"); if (loginUser == null) { resp.sendRedirect(req.getContextPath() + "/login.html"); return; } chain.doFilter(request, response); } }

这里有两个容易踩的坑:一是把登录接口自己也拦截了,导致用户永远无法登录,必须在过滤器里放行登录相关的URL;二是用了req.getSession(true)导致未登录请求也会创建一个Session,浪费内存。getSession(false)才是只获取已有Session不主动创建。

6.3 XSS攻击防护过滤器(含上传PDF场景)

XSS过滤是很多安全需求里绕不开的一环。简单说,XSS攻击是攻击者往页面里注入恶意脚本,而过滤器能做的就是统一清洗请求参数,把危险的HTML标签和脚本关键字转义掉。

要实现对参数值的清洗,光写普通过滤器不够,因为Servlet接收参数的方法是getParameter,而这个方法由ServletRequest对象提供,无法在Filter里直接修改它的行为。所以要用装饰器模式,用HttpServletRequestWrapper包装原始请求,重写getParameter、getParameterValues、getHeader等方法,在返回给业务代码前做转义。

package com.example.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.IOException; @WebFilter(urlPatterns = "/*") public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { XssRequestWrapper xssRequest = new XssRequestWrapper((HttpServletRequest) request); chain.doFilter(xssRequest, response); } public class XssRequestWrapper extends HttpServletRequestWrapper { public XssRequestWrapper(HttpServletRequest request) { super(request); } @Override public String getParameter(String name) { return clean(super.getParameter(name)); } @Override public String[] getParameterValues(String name) { String[] values = super.getParameterValues(name); if (values == null) { return null; } String[] cleaned = new String[values.length]; for (int i = 0; i < values.length; i++) { cleaned[i] = clean(values[i]); } return cleaned; } @Override public String getHeader(String name) { return clean(super.getHeader(name)); } private String clean(String value) { if (value == null) { return null; } return value .replaceAll("<", "&lt;") .replaceAll(">", "&gt;") .replaceAll("\"", "&quot;") .replaceAll("'", "&#39;") .replaceAll("\\(", "&#40;") .replaceAll("\\)", "&#41;"); } } }

这里有一个很重要的知识点:包装后的request对象必须传给chain.doFilter,让后续的Servlet和接口方法拿到的都是这个包装后的request。如果传了原始request,清洗逻辑就完全没生效,这个问题排查起来非常隐蔽。

有人会问“Spring Boot全局过滤器处理上传PDF文件时XSS攻击”怎么弄。这里要分清两个层面:上传PDF时,真正被服务端读取的元信息主要是文件名Content-Disposition等header,表单字段可以按上面的方式过滤;PDF文件本身是二进制数据,它的XSS风险通常不在Servlet层——而在于下载PDF时浏览器解析方式和文件内容里有没有恶意的JavaScript。Servlet过滤器能解决的是文件名、参数、Header的清洗;PDF文件内容的扫描需要引入专业的恶意文件检测组件,这已经是另一个技术领域了。

6.4 日志过滤器与性能监控

日志过滤器用来统一记录每个请求的路径、方法、耗时和客户端IP,在排查线上问题时特别有用。

package com.example.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @WebFilter(urlPatterns = "/*") public class LogFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; long start = System.currentTimeMillis(); try { chain.doFilter(request, response); } finally { long cost = System.currentTimeMillis() - start; System.out.println("[" + req.getMethod() + "] " + req.getRequestURI() + ", IP=" + req.getRemoteAddr() + ", 耗时=" + cost + "ms"); } } }

注意这里用到了try-finally,目的是保证即使后续Servlet抛出异常,耗时日志也能正常打印,这是日志过滤器里很重要的一个细节。如果只是把日志写在chain.doFilter后面,一旦Servlet报错,日志就不会输出,刚好把最有价值的错误日志丢掉了。

6.5 布隆过滤器在认证和反黑场景中的应用

布隆过滤器本身就是“过滤器”,不过它过滤的不是HTTP请求,而是“集合中的元素”。它牺牲了精度换速度:能极其快速地告诉你“一个元素一定不存在”或者“可能存在”,用内存换效率。通常会和Redis配合使用:布隆过滤器做第一层快速判断,Redis做第二层精确校验。

举个实际场景:接口要做IP黑名单过滤。如果黑名单有几百万条数据,每次都查Redis/数据库,QPS一高就容易扛不住。更合理的方式是把已知黑名单IP加载进布隆过滤器,过滤器判断“没有”就绝对放行,判断“有”再去Redis确认一次,双重校验既保证了准确性,又减少了存储层的压力。

package com.example.filter; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; @WebFilter(urlPatterns = "/*") public class BlackListFilter implements Filter { // 预计插入100万条数据,误判率设定为万分之一 private static final BloomFilter<String> BLACK_LIST = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 1000000, 0.0001); @Override public void init(FilterConfig config) { // 实际项目中可以从数据库/Redis加载黑名单,这里模拟添加一条 BLACK_LIST.put("127.0.0.1"); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String clientIp = req.getRemoteAddr(); if (BLACK_LIST.mightContain(clientIp)) { // 布隆过滤器说有,不代表一定有,还需精确确认 if (isRealBlackIp(clientIp)) { HttpServletResponse resp = (HttpServletResponse) response; resp.setStatus(HttpServletResponse.SC_FORBIDDEN); resp.setCharacterEncoding("UTF-8"); resp.getWriter().write("{\"code\":403,\"msg\":\"IP已被限制访问\"}"); return; } } chain.doFilter(request, response); } private boolean isRealBlackIp(String ip) { // 实际项目中这里做Redis查询,避免所有流量都打到Redis return "127.0.0.1".equals(ip); } }

布隆过滤器的核心参数就两个:预期元素数量和误判率。误判率设到0.0001(万分之一)已经比较保险,再低会导致占用空间显著增大,性价比不高。实际项目要记住的结论是:布隆过滤器只能用于“确定不存在”的快速排除,判断存在时必须配合精确存储(Redis缓存、数据库)二次确认。

6.6 Spring MVC和Spring Boot项目的过滤器注册

很多人用Spring Boot后就不再碰Servlet过滤器了,其实过滤器在Spring Boot里依然有不可替代的位置,只不过注册方式换成了FilterRegistrationBean,前面已经给过代码。

在Spring MVC阶段,项目里还有另一个选择:HandlerInterceptor(拦截器)。Spring Boot里Interceptor也是通过WebMvcConfigurer注册的,起作用的是Controller层。过滤器和拦截器最大的区别在于:过滤器工作在Servlet容器层,拦截器工作在Spring MVC框架层。

对比项Servlet FilterSpring HandlerInterceptor
处理层级Servlet容器层,在Servlet之前Spring MVC框架层,在Controller前后
能否处理静态资源可以,/*能拦到静态资源默认不拦截静态资源
能获取Spring容器Bean需要额外处理直接注入,天然是Spring管理
作用场景通用请求处理(编码、XSS、认证)Controller切面逻辑(权限、业务日志)

两个都各有优势。编码过滤、XSS清洗这类“请求清洗”工作在过滤器层更合适,因为它们在到达任何业务代码之前就该完成;而针对某个模块的权限校验、记录用户操作日志这类和具体业务相关的工作,用拦截器更合理,因为能拿到Controller的Method对象和Spring容器里的服务类。

7. 常见问题与排查技巧实录

7.1 过滤器不生效:最常见的四个原因

过滤器不生效这个问题,说真的,我见过太多次了。排查时可以按下面顺序快速定位:

第一,检查url-pattern写法。最常见的问题是把拦截所有请求写成“/* ”(多了一个空格),或者想拦截所有URL写成“/”。这两种都会导致过滤器没有任何请求能命中。

第二,检查注解有没有被容器扫描到。Spring Boot项目里用@WebFilter注解注册的过滤器,必须在启动类上加@ServletComponentScan,否则过滤器压根不会注册到容器里。这个问题特别隐蔽,因为编译不会报错,项目也能启动,就是不执行。

第三,检查过滤器类是不是被Spring容器重复管理了。如果在Spring Boot里用了@WebFilter加@Configuration双重注册,过滤器可能执行两次。解决办法是二选一,或者用FilterRegistrationBean统一管理。

第四,检查过滤器顺序。多个过滤器时,如果前面的过滤器直接把请求拦截了(比如认证失败重定向),后续过滤器自然不会执行。在定位问题时要看整个请求的过滤链,不要只盯一个过滤器。

7.2 POST请求体被读走:包装Request缓存Body

过滤器里如果想读取POST请求的body做日志记录或内容检查,读完之后再放行,Controller里就会拿到空的body。这是因为ServletRequest的InputStream是单向的流,读取一次就消耗完毕了,不能重新读。

解决办法和XSS过滤器一样,用HttpServletRequestWrapper包装请求,在构造时把body缓存到字节数组,重写getInputStream方法让它返回缓存数据。这样无论过滤器读了几次body,Controller还是能拿到完整请求体。

package com.example.filter; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; public class CacheBodyRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public CacheBodyRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 将原始请求体一次性读出并缓存 body = request.getInputStream().readAllBytes(); } @Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream buffer = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() { return buffer.read(); } @Override public boolean isFinished() { return buffer.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { throw new UnsupportedOperationException(); } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } }

用这个包装类包一层再放行,后续Controller和执行链都能正常读取body。这个技巧在做请求内容审计、签名校验、请求日志时会反复用到,建议收藏代码。

7.3 过滤器抛异常导致请求中断怎么办

如果过滤器的doFilter抛出了未捕获的异常,容器会把这个异常交给错误处理机制。默认情况下,Servlet容器会直接返回500错误页,包装后的请求后续代码不再执行,此时需要注意事务和资源释放问题。

建议在doFilter里做好异常分类:如果是业务校验异常(比如未登录、参数不合法),直接设置响应状态和提示信息后return,不要继续调用chain.doFilter;如果是系统级异常,尽量不吞掉,通过容器的全局异常处理机制统一处理,同时配合日志过滤器里的try-finally记录上下文的方确保日志不丢失。

7.4 过滤器与拦截器:什么时候用哪个

很多项目里过滤器、拦截器并存,经常有人搞混。我给一个很实际的判断标准:跟Servlet容器强相关的(如编码、跨域、XSS清洗、URL级别访问控制)用过滤器;跟Controller业务逻辑强相关的(如具体模块的权限校验、用户操作记录、性能监控的BizMetrics)用拦截器。

再重复一遍,过滤器在拦截器之前执行。如果两者配合使用,过滤器的chain.doFilter包含了后面所有逻辑,包括拦截器的preHandle、Controller、拦截器的postHandle和afterCompletion。理解这个时序,在排查“为什么过滤器的日志先打但拦截器的日志后打”之类的问题时就不会一头雾水。

结尾

最后再分享一个我自己项目里的习惯做法:写过滤器时,我会把所有过滤器统一放在filter包下,然后为每个过滤器写清楚它的职责和优先级。项目里必然有一个Readme记录过滤链的完整顺序和各过滤器的预期行为。过滤器这种“隐式执行”的代码,如果不主动维护文档,时间久了真的会忘记哪个过滤器做了什么,排查问题全靠猜。

如果在实际项目中遇到过滤器相关的诡异问题,逐层验证是最有效的方法。先确认过滤器有没有被注册,再确认请求路径有没有匹配上url-pattern,最后确认doFilter里有没有成功放行。90%的过滤器问题都逃不过这三步。

这个内容后续还可以怎么扩展?如果项目切换到Spring Cloud Gateway或微服务架构,你会发现网关层过滤器和Servlet过滤器有很多相似的理念——路由、过滤、限流、认证,都是一层一层“洋葱皮”式的处理逻辑。把Servlet过滤器的设计思路吃透,再去看网关的Filter和GlobalFilter会轻松很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 18:46:25

CentOS 7上用Docker部署Redis与PostgreSQL完整指南

最近在测试环境要搭一套缓存加关系型数据库的组合&#xff0c;顺手把整个流程从零到一完整走了一遍。今天就以 CentOS 7 为底&#xff0c;把 Docker 装好&#xff0c;再用 Docker 把 Redis 和 PostgreSQL 跑起来。整个过程其实就是一条命令链&#xff0c;但中间值得注意的坑不少…

作者头像 李华
网站建设 2026/9/24 18:45:58

原生Terraform vs 托管服务:ROS机器人项目IaC选型指南

1. 从一个真实的选择困境说起去年帮一个做机器人仿真平台的团队做基础设施评审&#xff0c;他们当时的状态特别典型&#xff1a;三个运维、两个ROS工程师&#xff0c;所有云上资源全靠手点控制台&#xff0c;测试环境重建一次要花大半天&#xff0c;还经常出现“这台机器有那个…

作者头像 李华
网站建设 2026/9/24 18:44:22

sysfs_fs_type(struct file_system_type)结构体

file_system_type是VFS与具体文件系统之间的桥梁&#xff0c;它定义了文件系统的名称、挂载/卸载行为、锁依赖关系以及所有已挂载实例的管理方式。每个文件系统只需提供一个这样的结构体&#xff0c;即可无缝接入VFS的统一框架

作者头像 李华
网站建设 2026/9/24 18:43:53

TransUnet眼底血管分割实战:拆解Transformer与U-Net缝合细节

简介&#xff1a;本资源是一套基于TransUnet架构实现眼底血管DRIVE数据集分割的完整实战方案&#xff0c;面向医学图像分割初学者与深度学习实践者&#xff0c;解决视网膜血管结构精准分割这一典型生物医学图像分析任务。压缩包共76个文件&#xff0c;含40张标注图像&#xff0…

作者头像 李华
网站建设 2026/9/24 18:43:42

2025终极指南:Jackett功能规划与未来路线图解析

2025终极指南&#xff1a;Jackett功能规划与未来路线图解析 还在为多Tracker管理烦恼&#xff1f;一文掌握Jackett 2025年核心升级方向&#xff0c;让你的媒体库管理效率提升300%&#xff01;读完本文你将了解&#xff1a; 下一代索引器架构如何解决80%的Tracker连接问题AI驱…

作者头像 李华
网站建设 2026/9/24 18:43:36

本地餐饮同城外卖系统开发,多门店订单管理技术方案

本地餐饮同城外卖系统开发&#xff0c;多门店订单管理技术方案连锁餐饮、多商户入驻的同城外卖平台&#xff0c;会面临多门店订单统一归集、分单、库存、出餐管控等问题。很多简易外卖系统采用单店独立模式&#xff0c;门店数据相互隔离&#xff0c;无法实现跨店统筹&#xff1…

作者头像 李华