CORS 跨域排查实战:从预检请求到彻底解决

浏览器报错 CORS 跨域几乎是每个前后端开发者都绕不开的坎:本地跑得好好的接口,一上线就冒出红色 Access-Control-Allow-Origin 警告,预检 OPTIONS 请求被拒、带 Cookie 的请求莫名失败。本文从同源策略讲起,用一张对照表梳理最常见的 5 类报错与根因,并给出 Nginx、Spring Boot、Node 的可直接复制配置,帮你把 CORS 跨域问题一次性彻底解决。

一、CORS 到底在防什么:同源策略

同源策略(Same-Origin Policy)是浏览器的安全基石:当协议、域名、端口三者任意一个不同,就视为跨源。跨源的前端脚本默认不能读取后端响应,CORS(Cross-Origin Resource Sharing,跨域资源共享)则是服务端用一组响应头,主动声明「允许哪些源、哪些方法、哪些头部」来放宽这条限制。它不是前端能单方面绕过的,必须由服务端配合。

二、预检请求(Preflight)为何而来

对于「简单请求」,浏览器直接发正式请求、再看响应头决定是否放行;但对于可能改数据的「非简单请求」,浏览器会先发一个 OPTIONS 方法的预检请求,确认服务端真的允许,才发真实请求。理解这一点,就明白为什么控制台里凭空多出一个 OPTIONS。

哪些情况会触发 OPTIONS 预检

满足以下任一条件即触发预检:使用了 PUT、DELETE、PATCH 等非 GET/POST 方法;Content-Type 为 application/json、application/xml 等非表单类型;自定义了请求头(如 Authorization、X-Token);或带凭证且服务端返回了不匹配的头。反之,普通表单 POST(application/x-www-form-urlencoded)往往是简单请求,不会预检。

用 curl 亲手模拟一次预检

调试时最直观的方法,是绕过浏览器用 curl 复现预检,看服务端到底回了什么头:

curl -i -X OPTIONS 'https://api.example.com/v1/order' \
  -H 'Origin: https://shop.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type,authorization'

# 关注返回头里是否包含:
#   access-control-allow-origin: https://shop.example.com
#   access-control-allow-methods: GET,POST,OPTIONS
#   access-control-allow-headers: content-type,authorization

三、最常见的 5 类报错与根因

把现象、根因、解法对齐,排错效率立刻翻倍:

现象(控制台/网络面板)根因解法
No ‘Access-Control-Allow-Origin’ header服务端没返回允许源后端加 Access-Control-Allow-Origin
Method %s is not allowedallow-methods 未包含该动词补上 PUT/DELETE/OPTIONS 等方法
Request header field x-token is not allowedallow-headers 缺自定义头把自定义头加入允许列表
Preflight response is not successful (403/404)网关/反代拦截了 OPTIONSNginx 放行 OPTIONS 并回 204
The value of ‘Expose-Headers’ / credentials 冲突带凭证时 allow-origin 不能是 *显式写死具体源,且 allow-credentials: true

四、服务端正确配置:三种常见栈

Nginx 加响应头(含预检放行)

在反代 location 内统一加头,并单独处理 OPTIONS 预检,直接返回 204 避免穿透到后端。若你的站点已用 Nginx 反向代理 托管多个服务,把这段放进对应 server 块即可。

location /api/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin 'https://shop.example.com' always;
        add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,OPTIONS' always;
        add_header Access-Control-Allow-Headers 'content-type,authorization,x-token' always;
        add_header Access-Control-Allow-Credentials 'true' always;
        add_header Content-Length 0;
        return 204;
    }
    add_header Access-Control-Allow-Origin 'https://shop.example.com' always;
    add_header Access-Control-Allow-Credentials 'true' always;
    proxy_pass http://127.0.0.1:8080;
}

Spring Boot:全局配置优于注解

用 @CrossOrigin 注解虽快,但散落各处难维护;推荐实现 WebMvcConfigurer 做全局放行。注意 allow-credentials 为 true 时,allowedOriginPatterns 不能写 *。

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOriginPatterns("https://shop.example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

Node / Express 显式设置头

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://shop.example.com');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

五、带凭证的请求(credentials)坑点

当 fetch 设置 credentials: 'include' 或 axios 设 withCredentials: true 想带上 Cookie 时,规则立刻变严:Access-Control-Allow-Origin 必须写死具体源,不能是通配符 *,且必须同时返回 Access-Control-Allow-Credentials: true,前端才能拿到响应。写成 * 是新手最常踩的隐形坑——浏览器会直接丢弃响应。

// 前端:带凭证调用
fetch('https://api.example.com/v1/profile', {
  method: 'GET',
  credentials: 'include',  // 关键:带上 Cookie
  headers: { 'Content-Type': 'application/json' }
});

六、开发环境用代理绕过,而非关掉安全

本地联调时,前后端端口不同天然跨域。与其在后端临时放开,不如用构建工具的反代把请求「变」成同源。Hono、Vue3 等现代前端栈都支持 devServer 代理,生产环境再用上文的服务端头兜底。

// vite.config.ts —— 本地把 /api 代理到后端,规避 CORS
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (p) => p.replace(/^\/api/, '')
      }
    }
  }
});

七、生产排错清单

检查项要点
预检是否放行OPTIONS 必须返回 2xx,且带 allow-* 头
Origin 是否精确匹配带凭证时禁止用 *,写死具体域名
自定义头是否声明Authorization、X-Token 等需进 allow-headers
网关层是否拦截Nginx/CDN/WAF 别把 OPTIONS 当异常丢弃
凭证一致性前端 include + 后端 credentials:true 同时成立
缓存预检结果maxAge 设 600~3600 秒,减少重复 OPTIONS

把这张清单过一遍,90% 的 CORS 跨域问题都能在十分钟内切掉。它和 微前端架构 的跨应用调用、以及各类 BFF 网关配置是同一套心智模型——本质都是「谁在边界上替你签发允许凭证」。

结语

CORS 不是敌人,而是浏览器替你守住的一道门。理解同源策略与预检机制后,它不过是一组需要正确填写的响应头。下次再见到红色报错,先 curl 复现预检、再对表定位,比盲目加 * 安全得多。

上一篇 Apache Paimon 实战:流式湖仓一体与实时数仓落地
下一篇 React 19 实战:用 Actions 与 useOptimistic 重塑前端交互
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]