十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 \ -DgroupIdcom.example \ -DartifactIdservlet-filter-demo \ -DarchetypeArtifactIdmaven-archetype-webapp \ -DinteractiveModefalse3.3 引入Servlet依赖和Tomcat部署生成的项目里打开pom.xml加上Servlet依赖。这里有个关键点scope要写成provided因为Servlet API由Tomcat提供如果打成war包还把Servlet API打包进去会和容器自带的类冲突。dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- Tomcat 10及以上改用 -- !-- dependency groupIdjakarta.servlet/groupId artifactIdjakarta.servlet-api/artifactId version5.0.0/version scopeprovided/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-namehelloFilter/filter-name filter-classcom.example.filter.HelloFilter/filter-class init-param param-namename/param-name param-valuehello/param-value /init-param /filter filter-mapping filter-namehelloFilter/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 FilterRegistrationBeanEncodingFilter encodingFilter() { FilterRegistrationBeanEncodingFilter registrationBean new FilterRegistrationBean(); registrationBean.setFilter(new EncodingFilter()); registrationBean.addUrlPatterns(/*); registrationBean.setOrder(1); return registrationBean; } Bean public FilterRegistrationBeanAuthFilter authFilter() { FilterRegistrationBeanAuthFilter registrationBean new FilterRegistrationBean(); registrationBean.setFilter(new AuthFilter()); registrationBean.addUrlPatterns(/user/*); registrationBean.setOrder(2); return registrationBean; } }这个方案的关键在于编码过滤器优先级最高order1确保任何请求在进入后续业务逻辑前字符集都已经正确安全过滤器排第二登录认证最后。逻辑层次清楚排查问题也直观。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 BloomFilterString 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方法让它返回缓存数据。这样无论过滤器读了几次bodyController还是能拿到完整请求体。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会轻松很多。
返回列表