CORS 深度解析与常见陷阱排查
David Ng | 2026-09-02T01:07:46 | Frontend, Security
从浏览器同源策略出发,全面解析 CORS 预检请求、凭证请求、通配符限制等机制,以及开发中最常见的 CORS 错误排查方法。
# CORS 深度解析与常见陷阱排查 ## 同源策略回顾 浏览器的同源策略(Same-Origin Policy)阻止网页访问不同源(协议+域名+端口)的资源。CORS(Cross-Origin Resource Sharing)是 W3C 标准,通过 HTTP 头部告诉浏览器允许哪些跨源请求。 ## 简单请求 vs 预检请求 ### 简单请求 满足以下所有条件的请求不会触发预检(preflight): 1. 方法:GET、HEAD、POST 2. 头部仅限:Accept、Accept-Language、Content-Language、Content-Type 3. Content-Type 仅限:text/plain、multipart/form-data、application/x-www-form-urlencoded ``` GET /api/data HTTP/1.1 Origin: https://app.example.com HTTP/1.1 200 OK Access-Control-Allow-Origin: https://app.example.com ``` ### 预检请求 不满足简单请求条件的会先发 OPTIONS 预检: ``` OPTIONS /api/data HTTP/1.1 Origin: https://app.example.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Content-Type, Authorization HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 86400 ``` ## Spring Boot 配置 ```java @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://app.example.com", "https://admin.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("Content-Type", "Authorization", "X-Request-Id") .exposedHeaders("X-Total-Count", "X-Request-Id") .allowCredentials(true) .maxAge(3600); } } ``` ## 常见陷阱 ### 陷阱 1: 通配符 + 凭证 ``` # 这是错误的! Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true ``` 浏览器会拒绝这种组合。使用凭证时必须指定具体域名。 解决方案 — 动态设置 Origin: ```java @Component public class DynamicCorsFilter implements Filter { private final Set allowedOrigins = Set.of( "https://app.example.com", "https://admin.example.com" ); @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String origin = request.getHeader("Origin"); if (origin != null && allowedOrigins.contains(origin)) { response.setHeader("Access-Control-Allow-Origin", origin); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Vary", "Origin"); } if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(204); return; } chain.doFilter(req, res); } } ``` ### 陷阱 2: 忘记处理 OPTIONS 很多后端框架的安全拦截器会拦截 OPTIONS 请求返回 401/403。 ```java // SecurityConfig 中放行 OPTIONS @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/api/admin/**").authenticated() .anyRequest().permitAll() ); return http.build(); } ``` ### 陷阱 3: Nginx 反代覆盖头部 如果 Nginx 和应用都设置了 CORS 头部,可能出现重复: ``` # 浏览器看到的响应头 Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Origin: https://app.example.com # 重复! ``` 浏览器会拒绝重复的 CORS 头部。解决:只在一个层设置。 ```nginx # Nginx: 只在应用没设置时添加 location /api/ { proxy_pass http://backend; # 如果后端已经设置了 CORS 头,这里不要重复设置 # 或者在这里统一设置,后端不设置 } ``` ### 陷阱 4: 自定义头部未声明 ```javascript // 前端想读取自定义响应头 const total = response.headers.get("X-Total-Count"); // null! ``` 需要服务端通过 `Access-Control-Expose-Headers` 声明: ``` Access-Control-Expose-Headers: X-Total-Count, X-Request-Id ``` ### 陷阱 5: Cookie 跨域携带 ```javascript // fetch 必须设置 credentials fetch("https://api.example.com/data", { credentials: "include" // 不是默认值! }); // axios 必须设置 withCredentials axios.get("https://api.example.com/data", { withCredentials: true }); ``` 同时服务端必须: - `Access-Control-Allow-Credentials: true` - `Access-Control-Allow-Origin` 不能是 `*` ## 调试清单 1. 打开 DevTools Network 面板,找到失败的请求 2. 检查是否有 OPTIONS 预检请求,状态码是否为 2xx 3. 检查响应头是否包含 Access-Control-Allow-Origin 4. 确认 Origin 值是否匹配(注意端口号) 5. 使用 `curl -X OPTIONS -H "Origin: ..." -v` 模拟预检请求 ## 总结 CORS 问题表面简单,但陷阱众多。理解简单请求和预检请求的区别是基础,避免通配符与凭证的冲突是重点。最佳实践是在一个层(应用或反代)统一管理 CORS 配置,不要两边都设。