logo

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 应用后的路径完整画出来:

  1. 客户端请求
  2. ↓
  3. Web 容器(如 Tomcat)
  4. ↓
  5. Filter 链(过滤器)
  6. ↓
  7. DispatcherServlet(前端控制器)
  8. ↓
  9. Interceptor 链(拦截器)
  10. ↓
  11. Controller(控制器)
  12. ↓
  13. Service / DAO
  14. ↓
  15. 返回响应,反向经过 Interceptor → Filter → 客户端

这条链路揭示了几个关键事实:

  • 过滤器位于容器层,在请求进入 Servlet 之前、响应离开 Servlet 之后生效,属于 Servlet 规范的一部分。
  • 拦截器位于框架层,由前端控制器调度,只能拦截进入 Controller 的请求。
  • 两者都是环绕式的:请求阶段按顺序进入,响应阶段按相反顺序退出。

理解这条链路,后面的所有差异都可以由此推导。


二、过滤器(Filter)深度解析

2.1 本质与生命周期

过滤器是 Servlet 规范定义的组件,依赖 Web 容器运行。它的生命周期由容器管理:

  1. 初始化:容器启动时调用 init(FilterConfig),只执行一次。
  2. 过滤:每次请求匹配到该过滤器时调用 doFilter(request, response, chain)。
  3. 销毁:容器关闭时调用 destroy()。

核心方法只有一个 doFilter,其中 chain.doFilter() 是分水岭:调用之前的代码属于请求预处理,调用之后的代码属于响应后处理。

  1. public class LoggingFilter implements Filter {
  2. @Override
  3. public void doFilter(ServletRequest req, ServletResponse resp,
  4. FilterChain chain) throws IOException, ServletException {
  5. long start = System.currentTimeMillis();
  6. // 请求前置处理
  7. chain.doFilter(req, resp); // 放行,进入下一个过滤器或 Servlet
  8. // 响应后置处理
  9. System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
  10. }
  11. }

2.2 能力边界

过滤器能做的事情比很多人想象的要多:

  • 修改请求与响应对象:通过包装类(如 HttpServletRequestWrapper)可以改写请求参数、请求头、请求体,这是拦截器做不到的。
  • 处理非 Controller 请求:静态资源、其他 Servlet 的请求同样会经过过滤器。
  • 在请求进入框架之前介入:这意味着即使请求最终没有匹配到任何 Controller,过滤器依然会执行。
  • 响应阶段改写内容:可以压缩响应、加密响应、统一添加响应头。

正因为能触碰原始请求与响应对象,CORS 相关的响应头设置通常必须在过滤器中完成——这一点在后文会详细展开。

2.3 典型适用场景

  • 全局字符编码设置
  • 跨域响应头注入
  • 请求/响应日志与耗时统计
  • 请求体缓存与重复读取
  • 敏感信息脱敏、XSS 基础防护

三、拦截器(Interceptor)深度解析

3.1 本质与执行时机

拦截器是框架层的扩展点,由前端控制器统一调度,本质上是一个普通的 Bean,可以方便地注入其他依赖。它围绕 Controller 方法的调用展开,核心回调有三个:

  • preHandle:Controller 方法执行前调用,返回 false 则中断后续流程。
  • postHandle:Controller 方法执行后、视图渲染前调用。
  • afterCompletion:整个请求完成后调用,常用于资源清理与异常记录。
  1. public class AuthInterceptor implements HandlerInterceptor {
  2. @Override
  3. public boolean preHandle(HttpServletRequest req, HttpServletResponse resp,
  4. Object handler) {
  5. String token = req.getHeader("Authorization");
  6. if (token == null || !validate(token)) {
  7. resp.setStatus(401);
  8. return false; // 中断,不再进入 Controller
  9. }
  10. return true;
  11. }
  12. }

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

最常见的原因有三类:

  1. 服务端根本没配置跨域响应头。
  2. 配置了跨域,但请求在到达跨域处理逻辑之前就被拦截(例如鉴权过滤器先返回了 401,响应里自然没有跨域头)。
  3. 预检请求被拦截器或安全框架拦截,未走到跨域处理逻辑。

第 2、3 点是实践中最容易被忽略的:跨域头必须加在最终返回的响应上,包括错误响应。

报错二:预检请求失败(OPTIONS 返回 403/405)

预检请求是 OPTIONS 方法,且不携带业务 Header。如果框架的鉴权逻辑要求所有请求必须携带 Token,预检请求必然失败。解决方式是在过滤器最前端放行 OPTIONS 请求,直接返回 200 并附带跨域头。

报错三:自定义 Header 跨域失败

如果前端发送了 X-Trace-Id 之类的自定义头,服务端的 Access-Control-Allow-Headers 必须显式列出该头,或使用通配配置。同时 Access-Control-Allow-Headers 存在大小写不敏感但必须完整覆盖的要求,遗漏任何一个都会导致预检失败。

报错四:Cookie 跨域不携带

这是最隐蔽的一类。要让跨域请求携带 Cookie,需要同时满足:

  1. 前端设置 withCredentials = true(或对应请求库的等价配置)。
  2. 服务端 Access-Control-Allow-Credentials: true。
  3. 服务端 Access-Control-Allow-Origin 必须是具体域名,不能是 *。
  4. Access-Control-Allow-Headers 与 Access-Control-Allow-Methods 同样不能使用 *。

任何一条不满足,浏览器都会静默丢弃 Cookie,表现为“登录状态丢失”。

5.3 基于过滤器的通用实现

由于跨域头必须覆盖所有响应(包括异常响应),且需要放行预检请求,过滤器是最合适的落点:

  1. public class CorsFilter implements Filter {
  2. private static final String ALLOWED_ORIGIN = "https://example.com";
  3. @Override
  4. public void doFilter(ServletRequest req, ServletResponse resp,
  5. FilterChain chain) throws IOException, ServletException {
  6. HttpServletRequest request = (HttpServletRequest) req;
  7. HttpServletResponse response = (HttpServletResponse) resp;
  8. String origin = request.getHeader("Origin");
  9. if (origin != null) {
  10. response.setHeader("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
  11. response.setHeader("Access-Control-Allow-Credentials", "true");
  12. response.setHeader("Access-Control-Allow-Methods",
  13. "GET, POST, PUT, DELETE, OPTIONS");
  14. response.setHeader("Access-Control-Allow-Headers",
  15. "Content-Type, Authorization, X-Trace-Id");
  16. response.setHeader("Access-Control-Max-Age", "3600");
  17. }
  18. // 预检请求直接放行,不进入后续过滤器与业务逻辑
  19. if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
  20. response.setStatus(HttpServletResponse.SC_OK);
  21. return;
  22. }
  23. chain.doFilter(req, resp);
  24. }
  25. }

几个关键设计点:

  • 放行 OPTIONS 必须在最前面,避免被后续鉴权逻辑拦截。
  • Access-Control-Allow-Origin 使用具体域名而非 *,因为需要携带 Cookie。
  • Access-Control-Max-Age 缓存预检结果,减少 OPTIONS 请求次数,降低延迟。
  • 如果需要支持多域名,应在白名单中校验 Origin 后再回写,而不是无脑回显,避免安全风险。

5.4 与框架自带跨域配置的关系

主流框架通常提供声明式跨域配置。它的优点是简洁,但需要注意:

  • 框架级配置一般只作用于进入框架的请求,异常响应、未匹配请求可能不生效。
  • 当过滤器与框架配置同时存在时,可能出现响应头重复,导致浏览器报“header 包含多个值”的错误。

因此推荐的做法是:统一在一处配置跨域。如果业务中已有复杂的过滤器链,优先在过滤器中集中处理,避免多头配置。

5.5 排查清单

遇到跨域问题时,按以下顺序排查效率最高:

  1. 打开浏览器开发者工具,确认是预检失败还是真实请求失败。
  2. 检查预检响应(OPTIONS)中是否包含完整的跨域头。
  3. 确认 Allow-Origin、Allow-Headers、Allow-Methods 是否与实际请求匹配。
  4. 若需携带 Cookie,检查是否使用了 *,是否设置 Allow-Credentials。
  5. 检查是否有鉴权逻辑在跨域处理之前拦截了请求。
  6. 检查是否存在重复的跨域头。

六、总结

过滤器与拦截器的差异,本质上是容器层与框架层的差异。过滤器更底层、覆盖面更广、能改写原始请求响应;拦截器更贴近业务、能感知方法信息、依赖注入更自然。二者不是替代关系,而是互补关系。

CORS 跨域的本质是浏览器行为,服务端能做的只有“正确告知浏览器是否放行”。由于跨域头必须覆盖包括异常在内的所有响应、且需要放行预检请求,过滤器是实现跨域的统一且可靠落点。而实践中绝大多数跨域疑难问题,最终都能归结为三句话:预检没放行、头没加全、带 Cookie 时用了通配符。把这三句话对应到具体配置上,跨域问题就不再神秘。

发表评论

活动