Java过滤器与拦截器及跨域问题解析
作者:很菜不狗2026.11.07 02:19浏览量:0简介:本文系统梳理过滤器与拦截器的执行时机、作用范围与适用场景,帮助你彻底厘清二者的职责边界;同时给出CORS跨域问题的完整解决思路,覆盖预检请求失败、自定义Header跨域、Cookie不携带等高频痛点,让跨域调试不再无从下手。
Java 开发:过滤器(Filter)、拦截器(Interceptor)深度解析与 CORS 跨域完整解决方案
在 Java Web 开发中,过滤器与拦截器是两个极其常见、却又极易混淆的组件。很多开发者能说出“过滤器基于 Servlet 规范、拦截器基于框架”,但一旦涉及具体问题——谁先执行、谁能拿到请求体、谁能在响应阶段生效、跨域请求为什么被拦截——就说不清楚了。与此同时,CORS 跨域几乎每个前后端分离项目都会遇到,报错信息往往只有一句 No 'Access-Control-Allow-Origin' header,却对应着完全不同的成因。
本文将从执行链路出发,逐层拆解过滤器与拦截器的差异,并在此基础上给出 CORS 跨域的完整解决方案。
一、先建立全局视角:一个请求的完整旅程
要理解过滤器与拦截器,最有效的方式是把一次 HTTP 请求进入 Java 应用后的路径完整画出来:
客户端请求↓Web 容器(如 Tomcat)↓Filter 链(过滤器)↓DispatcherServlet(前端控制器)↓Interceptor 链(拦截器)↓Controller(控制器)↓Service / DAO↓返回响应,反向经过 Interceptor → Filter → 客户端
这条链路揭示了几个关键事实:
- 过滤器位于容器层,在请求进入 Servlet 之前、响应离开 Servlet 之后生效,属于 Servlet 规范的一部分。
- 拦截器位于框架层,由前端控制器调度,只能拦截进入 Controller 的请求。
- 两者都是环绕式的:请求阶段按顺序进入,响应阶段按相反顺序退出。
理解这条链路,后面的所有差异都可以由此推导。
二、过滤器(Filter)深度解析
2.1 本质与生命周期
过滤器是 Servlet 规范定义的组件,依赖 Web 容器运行。它的生命周期由容器管理:
- 初始化:容器启动时调用
init(FilterConfig),只执行一次。 - 过滤:每次请求匹配到该过滤器时调用
doFilter(request, response, chain)。 - 销毁:容器关闭时调用
destroy()。
核心方法只有一个 doFilter,其中 chain.doFilter() 是分水岭:调用之前的代码属于请求预处理,调用之后的代码属于响应后处理。
public class LoggingFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse resp,FilterChain chain) throws IOException, ServletException {long start = System.currentTimeMillis();// 请求前置处理chain.doFilter(req, resp); // 放行,进入下一个过滤器或 Servlet// 响应后置处理System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");}}
2.2 能力边界
过滤器能做的事情比很多人想象的要多:
- 修改请求与响应对象:通过包装类(如
HttpServletRequestWrapper)可以改写请求参数、请求头、请求体,这是拦截器做不到的。 - 处理非 Controller 请求:静态资源、其他 Servlet 的请求同样会经过过滤器。
- 在请求进入框架之前介入:这意味着即使请求最终没有匹配到任何 Controller,过滤器依然会执行。
- 响应阶段改写内容:可以压缩响应、加密响应、统一添加响应头。
正因为能触碰原始请求与响应对象,CORS 相关的响应头设置通常必须在过滤器中完成——这一点在后文会详细展开。
2.3 典型适用场景
- 全局字符编码设置
- 跨域响应头注入
- 请求/响应日志与耗时统计
- 请求体缓存与重复读取
- 敏感信息脱敏、XSS 基础防护
三、拦截器(Interceptor)深度解析
3.1 本质与执行时机
拦截器是框架层的扩展点,由前端控制器统一调度,本质上是一个普通的 Bean,可以方便地注入其他依赖。它围绕 Controller 方法的调用展开,核心回调有三个:
preHandle:Controller 方法执行前调用,返回false则中断后续流程。postHandle:Controller 方法执行后、视图渲染前调用。afterCompletion:整个请求完成后调用,常用于资源清理与异常记录。
public class AuthInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest req, HttpServletResponse resp,Object handler) {String token = req.getHeader("Authorization");if (token == null || !validate(token)) {resp.setStatus(401);return false; // 中断,不再进入 Controller}return true;}}
3.2 能力边界
拦截器的优势在于贴近业务:
- 能拿到即将执行的 Handler 方法信息,可以基于注解做细粒度控制,例如只在标注了某个注解的方法上校验权限。
- 可以直接注入 Service、配置等 Bean,编写业务逻辑更自然。
- 可以精细控制请求的前后阶段。
它的局限同样明显:
- 无法修改请求体与响应体,因为它在框架内部、拿到的已经是解析后的对象。
- 无法处理未匹配到 Controller 的请求,静态资源、404 请求都不会进入拦截器。
- 无法参与响应内容的压缩、加密等字节级操作。
3.3 典型适用场景
- 登录态与权限校验
- 接口级别的限流与幂等控制
- 基于注解的个性化埋点
- 统一异常处理的前置补充
四、过滤器 vs 拦截器:一张表看清差异
| 维度 | 过滤器 Filter | 拦截器 Interceptor |
|---|---|---|
| 规范归属 | Servlet 规范 | 框架扩展点 |
| 依赖容器 | 是 | 否,依赖框架 |
| 执行位置 | Servlet 之前/之后 | Controller 之前/之后 |
| 触发范围 | 所有请求(含静态资源) | 仅进入 Controller 的请求 |
| 能否改请求/响应体 | 能,通过包装类 | 不能 |
| 能否获取 Handler 方法信息 | 不能 | 能 |
| 依赖注入 | 默认不支持(需特殊处理) | 原生支持 |
| 执行顺序 | 先于拦截器 | 后于过滤器 |
| 典型用途 | 编码、跨域、日志、压缩 | 鉴权、限流、埋点 |
一句话总结:需要触碰原始请求/响应、覆盖所有请求时用过滤器;需要贴近业务逻辑、基于方法做细粒度控制时用拦截器。
五、CORS 跨域完整解决方案
跨域问题的根源是浏览器的同源策略:协议、域名、端口三者任一不同即视为跨域,浏览器会限制跨域请求的响应读取。注意,请求往往已经发出去了,被拦的是响应——这也是很多开发者困惑“后端明明收到请求却报跨域”的原因。
5.1 简单请求与非简单请求
- 简单请求:方法为 GET/POST/HEAD,且请求头仅限安全字段,
Content-Type仅限三种特定值。浏览器直接发送,服务端返回Access-Control-Allow-Origin即可。 - 非简单请求:其余情况,如自定义 Header、
Content-Type: application/json、PUT/DELETE 方法。浏览器会先发一个 OPTIONS 预检请求,服务端必须正确响应预检,才会发送真实请求。
大多数跨域失败,都发生在预检阶段。
5.2 高频报错与成因
报错一:No 'Access-Control-Allow-Origin' header is present
最常见的原因有三类:
- 服务端根本没配置跨域响应头。
- 配置了跨域,但请求在到达跨域处理逻辑之前就被拦截(例如鉴权过滤器先返回了 401,响应里自然没有跨域头)。
- 预检请求被拦截器或安全框架拦截,未走到跨域处理逻辑。
第 2、3 点是实践中最容易被忽略的:跨域头必须加在最终返回的响应上,包括错误响应。
报错二:预检请求失败(OPTIONS 返回 403/405)
预检请求是 OPTIONS 方法,且不携带业务 Header。如果框架的鉴权逻辑要求所有请求必须携带 Token,预检请求必然失败。解决方式是在过滤器最前端放行 OPTIONS 请求,直接返回 200 并附带跨域头。
报错三:自定义 Header 跨域失败
如果前端发送了 X-Trace-Id 之类的自定义头,服务端的 Access-Control-Allow-Headers 必须显式列出该头,或使用通配配置。同时 Access-Control-Allow-Headers 存在大小写不敏感但必须完整覆盖的要求,遗漏任何一个都会导致预检失败。
报错四:Cookie 跨域不携带
这是最隐蔽的一类。要让跨域请求携带 Cookie,需要同时满足:
- 前端设置
withCredentials = true(或对应请求库的等价配置)。 - 服务端
Access-Control-Allow-Credentials: true。 - 服务端
Access-Control-Allow-Origin必须是具体域名,不能是*。 Access-Control-Allow-Headers与Access-Control-Allow-Methods同样不能使用*。
任何一条不满足,浏览器都会静默丢弃 Cookie,表现为“登录状态丢失”。
5.3 基于过滤器的通用实现
由于跨域头必须覆盖所有响应(包括异常响应),且需要放行预检请求,过滤器是最合适的落点:
public class CorsFilter implements Filter {private static final String ALLOWED_ORIGIN = "https://example.com";@Overridepublic void doFilter(ServletRequest req, ServletResponse resp,FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) resp;String origin = request.getHeader("Origin");if (origin != null) {response.setHeader("Access-Control-Allow-Origin", ALLOWED_ORIGIN);response.setHeader("Access-Control-Allow-Credentials", "true");response.setHeader("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS");response.setHeader("Access-Control-Allow-Headers","Content-Type, Authorization, X-Trace-Id");response.setHeader("Access-Control-Max-Age", "3600");}// 预检请求直接放行,不进入后续过滤器与业务逻辑if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {response.setStatus(HttpServletResponse.SC_OK);return;}chain.doFilter(req, resp);}}
几个关键设计点:
- 放行 OPTIONS 必须在最前面,避免被后续鉴权逻辑拦截。
Access-Control-Allow-Origin使用具体域名而非*,因为需要携带 Cookie。Access-Control-Max-Age缓存预检结果,减少 OPTIONS 请求次数,降低延迟。- 如果需要支持多域名,应在白名单中校验
Origin后再回写,而不是无脑回显,避免安全风险。
5.4 与框架自带跨域配置的关系
主流框架通常提供声明式跨域配置。它的优点是简洁,但需要注意:
- 框架级配置一般只作用于进入框架的请求,异常响应、未匹配请求可能不生效。
- 当过滤器与框架配置同时存在时,可能出现响应头重复,导致浏览器报“header 包含多个值”的错误。
因此推荐的做法是:统一在一处配置跨域。如果业务中已有复杂的过滤器链,优先在过滤器中集中处理,避免多头配置。
5.5 排查清单
遇到跨域问题时,按以下顺序排查效率最高:
- 打开浏览器开发者工具,确认是预检失败还是真实请求失败。
- 检查预检响应(OPTIONS)中是否包含完整的跨域头。
- 确认
Allow-Origin、Allow-Headers、Allow-Methods是否与实际请求匹配。 - 若需携带 Cookie,检查是否使用了
*,是否设置Allow-Credentials。 - 检查是否有鉴权逻辑在跨域处理之前拦截了请求。
- 检查是否存在重复的跨域头。
六、总结
过滤器与拦截器的差异,本质上是容器层与框架层的差异。过滤器更底层、覆盖面更广、能改写原始请求响应;拦截器更贴近业务、能感知方法信息、依赖注入更自然。二者不是替代关系,而是互补关系。
CORS 跨域的本质是浏览器行为,服务端能做的只有“正确告知浏览器是否放行”。由于跨域头必须覆盖包括异常在内的所有响应、且需要放行预检请求,过滤器是实现跨域的统一且可靠落点。而实践中绝大多数跨域疑难问题,最终都能归结为三句话:预检没放行、头没加全、带 Cookie 时用了通配符。把这三句话对应到具体配置上,跨域问题就不再神秘。

登录后可评论,请前往 登录 或 注册