资讯详情

Java Web Filter详解:从登录拦截到请求处理链路

📅 2026/10/12 22:54:12 | 华诺云谱 👁 阅读
Java Web Filter详解:从登录拦截到请求处理链路
1. 从一个登录拦截场景说起为什么需要Filter很多刚接触Java Web开发的朋友第一次听到过滤器这个词的时候脑子里浮现的可能是净水器里那层滤芯——脏水进去干净水出来。这个直觉其实相当准确。Filter在Java EE规范里的定位就是在请求到达目标资源之前、以及响应返回客户端之前插入一段可复用的处理逻辑。它不改变请求的本质但可以对请求和响应做预处理和后处理。我最初理解Filter是在做一个后台管理系统的时候。当时的需求很朴素除了登录页面本身其他所有页面都必须登录后才能访问。最笨的办法是在每个Servlet的doGet/doPost开头都写一段检查session里有没有用户信息没有就跳转到登录页。写了三个Servlet之后我就受不了了——重复代码太多而且一旦登录校验逻辑要改比如增加token校验得改几十个地方。这时候Filter就派上用场了写一个登录校验过滤器配置它拦截所有请求在doFilter方法里统一判断放行登录相关的路径即可。这个场景揭示了Filter存在的核心价值横切关注点的集中处理。所谓横切就是那些散落在多个模块中、跟业务逻辑本身关系不大的通用逻辑比如编码处理、登录校验、日志记录、敏感词过滤、跨域处理、请求参数清洗等等。这些逻辑如果混在业务代码里会让代码变得臃肿且难以维护。Filter提供了一种机制把这些逻辑抽出来统一在请求链路的某个位置执行。从技术层面看Filter是Servlet规范的一部分不是Spring发明的也不是某个框架特有的。只要你的应用跑在Servlet容器里Tomcat、Jetty、Undertow等Filter就能工作。这一点很重要意味着你理解了Filter就理解了Java Web请求处理链路的一个基础环节后面学Spring的Interceptor、AOP都能找到对应的参照。这篇文章我会从Filter的基本概念讲起然后带你写一个能跑起来的最小示例再深入讲配置方式、执行顺序、生命周期最后分享几个我实际踩过的坑。目标很明确看完之后你能独立写出可用的Filter并且知道它为什么这么写。2. Filter的核心概念拆解三个方法撑起一套机制2.1 Filter接口的三个生命周期方法Filter本质上就是一个接口位于javax.servlet包下如果你用的是Jakarta EE 9则是jakarta.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方法在过滤器实例被创建后调用一次容器会传入一个FilterConfig对象通过它可以读取在web.xml中配置的初始化参数。这个方法适合做一些一次性的准备工作比如加载配置文件、初始化连接池引用等。注意它只会执行一次不是每个请求都执行。doFilter是核心方法每次请求匹配到这个过滤器时都会调用。它接收三个参数ServletRequest、ServletResponse和FilterChain。前两个是请求和响应对象第三个是过滤器链这是理解Filter机制的关键。destroy方法在过滤器被销毁前调用一次用于释放资源比如关闭文件流、清理缓存等。通常Web应用停止时容器会调用它。这里有个细节值得注意init和destroy在接口里是default方法Java 8之后意味着你可以只实现doFilter。但在早期的Servlet规范中这三个方法都是抽象方法必须全部实现。如果你看一些老教程会发现它们把三个方法都写出来了这不是啰嗦是历史原因。2.2 FilterChain过滤器链的串联逻辑FilterChain是理解Filter执行流程的核心。你可以把它想象成一条流水线每个过滤器是流水线上的一个工位。请求从第一个工位进入经过处理后传递给下一个工位直到最后一个工位也就是目标Servlet或JSP。关键代码就一行chain.doFilter(request, response);这行代码的含义是把请求交给链上的下一个环节。如果你在doFilter方法里调用了它请求就会继续往下走如果你不调用请求就会在这里被截断后面的过滤器和目标资源都不会执行。这正是实现拦截功能的基础——登录校验不通过时直接return不调用chain.doFilter请求就到此为止了。但这里有个容易混淆的点chain.doFilter这行代码之前的逻辑是在请求到达目标资源之前执行的而这行代码之后的逻辑是在目标资源处理完、响应返回时执行的。因为方法调用是嵌套的像洋葱一样一层层剥开再一层层合上。public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 请求到达目标资源之前的处理 System.out.println(before: 请求进入); chain.doFilter(request, response); // 放行交给下一个环节 // 目标资源处理完、响应返回时的处理 System.out.println(after: 响应返回); }这个洋葱模型是Filter最精妙的地方。它让你可以在同一个方法里同时处理请求前置逻辑和响应后置逻辑而不需要写两个方法。比如统计请求耗时就可以在chain.doFilter之前记录开始时间之后记录结束时间两者相减即可。2.3 Filter与Servlet、Interceptor的区别初学者经常搞混Filter和Servlet以及后来Spring MVC里的Interceptor。我用一个表格来对比这样更直观。维度FilterServletInterceptorSpring MVC规范归属Servlet规范Servlet规范Spring框架作用位置请求进入容器后到达Servlet前请求的最终处理者在DispatcherServlet内部Controller之前能否获取Spring Bean默认不能需特殊处理默认不能能天然在Spring容器中执行顺序按配置顺序链式由URL映射决定按配置顺序链式典型用途编码、登录校验、日志、跨域处理具体业务请求权限、日志、参数校验简单说Filter更底层工作在Servlet容器层面能拦截几乎所有请求包括静态资源Interceptor更上层工作在Spring MVC层面只能拦截进入DispatcherServlet的请求但能方便地使用Spring的依赖注入。实际项目中两者经常配合使用Filter做全局的编码和粗粒度拦截Interceptor做细粒度的权限和业务相关处理。3. 手写第一个Filter从零到跑通的完整步骤3.1 环境准备与项目结构我假设你用的是Maven项目Servlet容器是Tomcat 9对应Servlet 4.0包名还是javax.servlet。如果你用的是Tomcat 10包名变成了jakarta.servlet代码里的import要相应调整这是很多人在升级时踩的第一个坑。pom.xml里需要引入Servlet API注意scope是provided因为Tomcat本身已经提供了这个jar包打包时不需要带进去dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency项目结构大致如下src/main/java/com/example/filter/ EncodingFilter.java LoginFilter.java src/main/webapp/ login.jsp index.jsp WEB-INF/web.xml3.2 编写一个编码过滤器中文乱码是Java Web开发中最经典的问题之一。请求参数里的中文如果编码不对到了后端就变成问号或者乱码。最省事的解决办法就是写一个编码过滤器统一设置请求和响应的字符集。package com.example.filter; import javax.servlet.*; import java.io.IOException; public class EncodingFilter implements Filter { private String encoding UTF-8; Override public void init(FilterConfig filterConfig) throws ServletException { // 从配置中读取编码参数如果没有配置则用默认的UTF-8 String param filterConfig.getInitParameter(encoding); if (param ! null !param.trim().isEmpty()) { this.encoding param; } System.out.println(EncodingFilter 初始化使用编码 encoding); } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 设置请求体的编码解决POST请求中文乱码 request.setCharacterEncoding(encoding); // 设置响应的编码和内容类型解决响应中文乱码 response.setCharacterEncoding(encoding); response.setContentType(text/html;charset encoding); // 放行交给下一个过滤器或目标资源 chain.doFilter(request, response); } Override public void destroy() { System.out.println(EncodingFilter 销毁); } }这段代码有几个点需要解释。request.setCharacterEncoding只对POST请求的请求体有效对GET请求的URL参数无效。GET请求的中文乱码需要在Tomcat的server.xml里配置URIEncoding或者用其他方式处理。这是很多人不知道的细节以为设了编码过滤器就万事大吉结果GET请求还是乱码。response.setContentType同时设置了内容类型和字符集这比单独调setCharacterEncoding更保险因为有些浏览器会优先看Content-Type头里的charset。3.3 编写一个登录校验过滤器接下来写一个更实用的登录过滤器拦截未登录的访问。package com.example.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.Arrays; import java.util.List; public class LoginFilter implements Filter { // 不需要登录就能访问的路径 private ListString excludePaths; Override public void init(FilterConfig filterConfig) throws ServletException { String excludes filterConfig.getInitParameter(excludePaths); if (excludes ! null) { excludePaths Arrays.asList(excludes.split(,)); } } Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); String contextPath request.getContextPath(); // 去掉上下文路径得到相对路径 String path uri.substring(contextPath.length()); // 白名单路径直接放行 if (excludePaths ! null) { for (String exclude : excludePaths) { if (path.startsWith(exclude.trim())) { chain.doFilter(request, response); return; } } } // 检查session中是否有登录标记 HttpSession session request.getSession(false); boolean loggedIn (session ! null session.getAttribute(user) ! null); if (loggedIn) { chain.doFilter(request, response); } else { // 未登录重定向到登录页 response.sendRedirect(contextPath /login.jsp); } } Override public void destroy() { excludePaths null; } }这里有个细节request.getSession(false)传入false表示如果session不存在就返回null不要新建。如果用getSession()无参版本它会自动创建session这样每个未登录请求都会创建一个新session浪费内存。这个坑我在早期项目里踩过后来看服务器内存监控才发现session数量异常增长。3.4 在web.xml中注册过滤器Filter写好了不会自动生效需要在web.xml里注册或者用注解。先看web.xml的方式filter filter-nameencodingFilter/filter-name filter-classcom.example.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping filter filter-nameloginFilter/filter-name filter-classcom.example.filter.LoginFilter/filter-class init-param param-nameexcludePaths/param-name param-value/login.jsp,/login,/static//param-value /init-param /filter filter-mapping filter-nameloginFilter/filter-name url-pattern/*/url-pattern /filter-mappingurl-pattern的写法有几种/*匹配所有请求/admin/*匹配admin下的所有路径*.jsp匹配所有jsp文件。注意/*和/的区别前者匹配所有包括子路径后者只匹配根路径这个容易写错。过滤器的执行顺序由filter-mapping在web.xml中出现的顺序决定而不是filter元素的顺序。所以上面配置中encodingFilter先执行loginFilter后执行。这个顺序很重要编码过滤器必须排在前面否则后面的过滤器读取参数时可能已经乱码了。3.5 用注解方式注册过滤器Servlet 3.0之后支持注解可以省去web.xml配置WebFilter( filterName encodingFilter, urlPatterns /*, initParams { WebInitParam(name encoding, value UTF-8) } ) public class EncodingFilter implements Filter { // ... }注解方式更简洁但有个限制注解方式无法指定过滤器的执行顺序。多个注解过滤器之间的顺序是不确定的取决于容器扫描类的顺序。如果顺序很重要比如编码过滤器必须最先执行还是得用web.xml或者用WebFilter配合编程式注册。这是注解方式的一个硬伤很多人用了注解之后发现编码过滤器偶尔不生效就是因为顺序问题。4. Filter的进阶用法与执行顺序实战4.1 多个过滤器的执行顺序验证光看文档说按配置顺序执行不够直观我写了个小实验来验证。定义三个过滤器A、B、C每个都在chain.doFilter前后打印日志。public class FilterA implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { System.out.println(A - before); chain.doFilter(req, resp); System.out.println(A - after); } } // FilterB、FilterC类似在web.xml中按A、B、C的顺序配置访问任意页面控制台输出是A - before B - before C - before 目标资源处理 C - after B - after A - after这个输出完美印证了洋葱模型请求阶段按配置顺序依次进入响应阶段按相反顺序依次返回。理解了这个你就能解释为什么编码过滤器要放最前面——它在请求阶段最先执行能保证后续所有环节拿到的都是正确编码的数据。4.2 过滤器的四种拦截方式配置filter-mapping里有个dispatcher元素用来指定过滤器拦截哪种请求分发方式。默认是REQUEST也就是客户端直接发起的请求。还有三种FORWARD通过RequestDispatcher.forward()转发的请求INCLUDE通过RequestDispatcher.include()包含的请求ERROR通过错误页面机制分发的请求这个配置在实际项目中很有用。比如登录过滤器如果只配了REQUEST那么当某个Servlet内部forward到另一个页面时过滤器不会再次执行。这通常是符合预期的但如果你希望forward也走过滤器就得显式配置。filter-mapping filter-nameloginFilter/filter-name url-pattern/*/url-pattern dispatcherREQUEST/dispatcher dispatcherFORWARD/dispatcher /filter-mapping我遇到过一个案例某项目用forward做页面跳转登录过滤器只配了REQUEST结果用户未登录时直接访问某个会forward的URL过滤器拦住了第一次请求但forward之后的页面没有被再次校验导致可以绕过登录。加上FORWARD配置后就正常了。这个坑比较隐蔽因为表面上看过滤器是生效的只是没覆盖到forward场景。4.3 FilterConfig读取初始化参数的细节FilterConfig除了getInitParameter还有几个方法值得了解getFilterName()返回过滤器的名称getServletContext()返回ServletContext可以访问应用级别的属性getInitParameterNames()返回所有初始化参数名的枚举初始化参数在init方法中读取一次即可不要在doFilter里反复读取因为FilterConfig在doFilter阶段是拿不到的除非你把它存成成员变量。我见过有人在doFilter里想读配置结果发现没有FilterConfig参数就是因为没理解生命周期。4.4 过滤器中的请求包装HttpServletRequestWrapperFilter的一个高级用法是包装请求对象修改请求参数。比如你想统一给所有请求参数做trim处理或者过滤敏感词直接改HttpServletRequest是做不到的它的getParameter方法没有setter。这时候需要继承HttpServletRequestWrapper。public class TrimRequestWrapper extends HttpServletRequestWrapper { public TrimRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); return value null ? null : value.trim(); } }然后在过滤器中public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest) { chain.doFilter(new TrimRequestWrapper((HttpServletRequest) request), response); } else { chain.doFilter(request, response); } }这个技巧在处理表单提交时特别有用用户输入的前后空格自动去掉省得在每个业务方法里手动trim。但要注意包装后的request传给chain.doFilter后续的过滤器和Servlet拿到的都是包装对象所以包装逻辑要放在合适的位置。5. 那些年我踩过的Filter坑5.1 过滤器不生效的排查思路过滤器配了但没反应这是新手最常见的问题。我总结了一个排查清单按顺序检查第一检查url-pattern是否匹配。/*匹配所有/只匹配根路径*.do匹配特定后缀。如果你写的是/admin那只有访问/admin这个精确路径才会触发/admin/user不会触发。要匹配子路径得写/admin/*。第二检查web.xml的版本声明。如果web.xml的version属性是2.3或更低注解方式不生效而且某些配置行为也不一样。建议用3.0以上。第三检查过滤器类是否被正确编译和部署。有时候改了代码没重新编译或者class文件没更新到WEB-INF/classes下过滤器用的还是旧版本。第四检查是否有多个过滤器互相影响。比如前面的过滤器没有调用chain.doFilter后面的过滤器就永远不会执行。第五检查dispatcher配置。如果请求是通过forward到达的而过滤器只配了REQUEST就不会触发。5.2 在过滤器中注入Spring Bean的正确姿势Filter由Servlet容器管理不在Spring容器里所以直接用Autowired注入Bean会失败得到null。这个问题困扰过很多人。解决办法有几种第一种用WebApplicationContextUtils从ServletContext中获取Spring上下文public void init(FilterConfig filterConfig) throws ServletException { ServletContext context filterConfig.getServletContext(); WebApplicationContext ctx WebApplicationContextUtils.getWebApplicationContext(context); this.userService ctx.getBean(UserService.class); }第二种用Spring的DelegatingFilterProxy把过滤器配置成Spring Bean然后在web.xml中代理。这种方式更优雅但配置稍复杂。第三种如果用的是Spring Boot直接把Filter注册为BeanSpring Boot会自动处理依赖注入和注册。我个人的建议是如果项目用Spring Boot就用Bean方式如果是传统SSM项目用第一种方式最直接。5.3 过滤器中的异常处理doFilter方法抛出的异常会直接传播到容器容器会显示错误页面。如果你希望在过滤器里捕获异常并做统一处理比如返回JSON错误信息需要自己try-catch。public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { chain.doFilter(request, response); } catch (Exception e) { // 统一异常处理 response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:500,\msg\:\服务器内部错误\}); } }但要注意如果响应已经开始输出比如Servlet已经写了一部分内容再往response里写数据会报错。所以这种统一异常处理更适合在请求早期阶段使用或者配合response.isCommitted()判断。5.4 过滤器与静态资源的关系默认情况下/*会拦截所有请求包括CSS、JS、图片等静态资源。如果登录过滤器拦截了静态资源未登录用户连登录页的样式都加载不出来页面会很难看。所以白名单里一定要加上静态资源路径比如/static/、/css/、/js/、/images/。另一个思路是在url-pattern里排除静态资源但Servlet的url-pattern不支持排除语法只能通过白名单在代码里判断。这是Filter配置的一个不便之处。6. 从Filter到请求处理链路一个完整的认知框架6.1 一次请求在容器中的完整旅程理解了Filter最好再把整个请求处理链路串起来看。当浏览器发起一个请求大致经历这些环节连接器接收请求解析HTTP报文根据URL找到对应的Host和ContextWeb应用在Context中匹配Filter链依次执行每个Filter的请求前置逻辑匹配到目标Servlet调用其service方法Servlet处理业务可能forward或include其他资源响应返回依次执行Filter链的响应后置逻辑连接器把响应写回客户端Filter在这个链路中处于第3步和第6步是请求进入业务逻辑前的最后一道关卡也是响应返回客户端前的最后一道处理。这个位置决定了它适合做全局性的、与业务无关的处理。6.2 Filter在典型项目中的配置清单根据我的经验一个中等规模的Java Web项目通常会配置这些Filter过滤器作用执行顺序CharacterEncodingFilter统一编码最前CorsFilter跨域处理靠前LogFilter请求日志记录靠前LoginFilter登录校验中间XssFilterXSS防护中间CompressionFilter响应压缩靠后顺序的原则是越基础的、越全局的越靠前越贴近业务的越靠后。编码和跨域是基础设置必须最先登录校验依赖前面的编码正确压缩是响应阶段的处理放最后。6.3 什么时候不该用FilterFilter虽然好用但也不是万能的。以下几种情况要慎重需要访问Spring Bean且逻辑复杂时优先考虑Interceptor因为Interceptor天然在Spring容器中依赖注入更方便。需要精确控制到方法级别的拦截时Filter做不到它只能基于URL模式匹配。方法级别的拦截要用AOP。需要修改请求参数时Filter需要包装request比较繁琐而Spring MVC的ControllerAdvice配合RequestBodyAdvice可能更合适。需要异步处理时Filter的同步模型可能成为瓶颈要考虑用异步Filter或者响应式方案。我个人的经验是Filter最适合做那些跟业务无关、跟请求本身有关的事情比如编码、日志、跨域、压缩。一旦逻辑开始涉及业务规则就该考虑往上层走。7. 写在最后的一点个人体会Filter这个知识点看起来简单三个方法而已但真正用好需要理解它的生命周期、执行顺序、与容器的关系。我见过不少工作两三年的开发者写Filter还是靠复制粘贴遇到不生效就到处试根本原因是没有理解FilterChain的洋葱模型和url-pattern的匹配规则。如果你刚开始学我的建议是不要只看不写。自己动手写一个编码过滤器、一个登录过滤器故意配错url-pattern看看什么效果故意不调chain.doFilter看看请求怎么被截断故意配两个过滤器看看执行顺序。这些实验花不了多少时间但能让你对Filter的理解从知道变成掌握。另外Servlet规范从3.0到4.0再到Jakarta EE 9的包名变更是很多人升级项目时的痛点。如果你维护的是老项目注意javax.servlet和jakarta.servlet的区别迁移时import要全改web.xml的schema也要更新。这个坑我在一次项目升级中踩过改了几十个文件才搞定。最后分享一个实用技巧调试Filter时在doFilter里打印请求的URI和当前过滤器的名字能快速定位是哪个过滤器拦截了请求、执行顺序是否符合预期。这个简单的日志在排查请求为什么没到Controller这类问题时比任何调试工具都直接。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑