前後端分離架構下,瀏覽器同源策略導致跨域請求被攔截。本文提供生產級 Servlet Filter 方案,在 Tomcat 級別統一處理 CORS 響應頭,徹底解決 Access-Control-Allow-Origin 配置失效、多頭配置混亂等問題。
package com.example.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 可讀取初始化參數:允許的域、方法、頭等
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 核心響應頭設置
httpResponse.addHeader("Access-Control-Allow-Origin", "*"); // 或具體域
httpResponse.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
httpResponse.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
httpResponse.addHeader("Access-Control-Allow-Credentials", "true"); // 支持 Cookie
httpResponse.addHeader("Access-Control-Max-Age", "3600"); // 預檢請求緩存 1小時
// 處理預檢請求 (OPTIONS) - 直接返回 200
if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) request).getMethod())) {
httpResponse.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 清理資源
}
}
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>com.example.filter.CorsFilter</filter-class>
<init-param>
<param-name>allowedOrigins</param-name>
<param-value>https://app.example.com,https://admin.example.com</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 攔截所有請求 -->
</filter-mapping>
若使用 Spring 生態,優先使用框架內置支持,避免重複造輪子:
// Spring MVC 配置類
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
// 或 Spring Boot 2.4+ 全局配置
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*").allowedMethods("*").allowedHeaders("*");
}
};
}
// 或 Controller 級別精細控制
@CrossOrigin(origins = "https://app.example.com", allowCredentials = "true")
@RestController
public class ApiController { ... }
Access-Control-Allow-Origin: * 與 Access-Control-Allow-Credentials: true 不能共存 (瀏覽器拒絕)。生產環境需動態校驗 Origin 請求頭:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String origin = req.getHeader("Origin");
// 白名單校驗 (可從配置中心/數據庫加載)
Set allowedOrigins = Set.of("https://app.example.com", "https://admin.example.com");
if (allowedOrigins.contains(origin)) {
res.addHeader("Access-Control-Allow-Origin", origin); // 返回具體域而非 *
res.addHeader("Access-Control-Allow-Credentials", "true");
}
res.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
res.addHeader("Access-Control-Max-Age", "3600");
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
res.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
| 方式 | 缺點 |
|---|---|
server.xml <Valve> / <Filter> | 容器級配置,耦合部署環境,難以動態化、項目隔離、版本控制 |
web.xml 內置 CorsFilter (Tomcat 7.0.41+) | 配置繁瑣,不支持動態域名、Spring 整合差 |
| 應用級 Filter (本文方案) | ✅ 代碼可控、動態配置、Spring 兼容、項目隔離、單元測試友好 |
| Spring 框架內置 CORS | ✅ 最佳實踐:聲明式、統一管理、支持預檢、與 Security 整合 |
Access-Control-Allow-Origin: * + Allow-Credentials: true → 瀏覽器報錯WebMvcConfigurer / @CrossOrigin,零侵入CorsFilter,配置動態域名白名單Origin 請求頭與 Access-Control-Allow-* 響應頭,curl -H "Origin: xxx" -v 驗證徹底搞定 CORS,關鍵在於理解預檢機制、區分簡單/非簡單請求、正確處理憑證模式、選對配置層級。拒絕在 server.xml 硬編碼,擁抱應用級可控方案。