跳到主要内容

CORS 跨域报错怎么修:把 Access-Control 响应头讲清楚

从浏览器为什么拦截跨域请求讲起,逐一拆解 Access-Control-Allow-Origin、预检 OPTIONS、允许的方法和请求头,再给一组真实可粘贴的 CORS 响应头配置示例。

发布于 作者 李雷
#CORS #跨域 #Access-Control #预检请求 #Nginx

CORS 跨域报错怎么修:把 Access-Control 响应头讲清楚

前端调接口,控制台突然蹦出一行红字:被 CORS 策略拦截。换个浏览器还是一样,改 fetch 参数也没用。这不是你的 JavaScript 写错了,是服务端没给浏览器一句许可。这篇把跨域的来龙去脉讲透,你看完就知道该往响应里加哪几个头。

为什么会跨域报错

浏览器有一条同源策略:协议、域名、端口三者完全相同才算同源。https://app.example.com 上的页面去读 https://api.example.com 的 JSON,域名不同,就是跨域。

这里有个常被误会的点:请求其实已经发到服务端,服务端也返回了数据,只是浏览器在把响应交给你的 JavaScript 之前拦了一道。它发现服务端没有声明放行,于是把响应内容藏起来,只留给你一个错误。所以跨域问题的钥匙不在前端,在服务端的响应头。

服务端放行的方式,就是在响应里返回 Access-Control 开头的那一组头,告诉浏览器:这个来源我认,放它读。

Access-Control-Allow-Origin 怎么填

这是最核心的一个头,决定谁能读到响应。它有两种写法。

第一种是通配 *,任何站点都能读。适合公开只读数据,比如一个不带 Cookie、不需鉴权的免费 JSON 接口。

第二种是写一个具体来源,例如 Access-Control-Allow-Origin: https://app.example.com,这样只有这一个站点能读,其它全挡在外面。

要特别记住一条红线:一旦请求要带凭证(Cookie、HTTP 鉴权、客户端证书),浏览器就拒绝把 和 Access-Control-Allow-Credentials: true 一起用。因为 代表任意网站,把已登录用户的 Cookie 发给任意网站,等于把私密数据交给恶意页面。这时只能回显发起请求的那一个具体来源,而且最好拿一份白名单校验过再回显。

预检 OPTIONS 请求是怎么回事

不是每个跨域请求都直接发出去。除了简单的 GET 和不带特殊头的 POST,浏览器在发真正的请求前会先偷偷发一个 OPTIONS,去问服务端能不能发,这一步叫预检。

服务端在预检的响应里用两个头回答:

  • Access-Control-Allow-Methods:我允许哪些方法,比如 GET、POST、PUT、DELETE。
  • Access-Control-Allow-Headers:我接受哪些请求头,比如 Content-Type、Authorization。

举个常踩的坑:你的 fetch 带 Authorization 发一个 PUT,预检就必须在 Allow-Methods 里含 PUT、在 Allow-Headers 里含 Authorization,缺一个,真正的请求根本发不出去。很多人遇到的现象是,普通 GET 本来好好的,一换成带鉴权头的 PUT 就失败,问题就出在这里。

预检还能缓存。Access-Control-Max-Age 告诉浏览器这份许可能存多少秒,设成 86400 就是一天内不必再问,省掉重复的 OPTIONS 往返。

一组真实的 CORS 响应头

把上面几条凑成一个完整场景:前端在 https://app.example.com,接口在 https://api.example.com,要带会话 Cookie,方法用到 GET 和 PUT,请求头带 Content-Type 和 Authorization,还想让前端读到分页用的 X-Total-Count。服务端该返回这一组:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Access-Control-Expose-Headers: X-Total-Count

逐行看:因为带凭证,Allow-Origin 写成具体来源而不是 ;Allow-Methods 把 PUT 和 OPTIONS 列上,预检才会通过;Allow-Headers 把两个请求头都放行;Allow-Credentials 为 true 让 Cookie 能跟着走;Max-Age 缓存一天;最后那行 Expose-Headers 是关键,默认前端只能读一小批安全名单里的头,自定义的 X-Total-Count 不列出来,response.headers.get('X-Total-Count') 就一直返回 null。手动一行行敲容易漏,我习惯先用 CORS 响应头生成器 勾选项生成,它还会在你同时勾上凭证和 时立刻标红,免得配出浏览器一定拒绝的组合。

写进 Nginx 时别漏 always

把这些头落到 Nginx,每一行是一个 add_header 指令:

add_header Access-Control-Allow-Origin https://app.example.com always;
add_header Access-Control-Allow-Methods "GET, PUT, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;

末尾的 always 很容易漏,但它很关键。少了它,CORS 头只在 2xx 响应上发,4xx 和 5xx 不发,结果浏览器连错误响应体都不让页面读,你排查接口报错时会更摸不着头脑。预检还要单独处理 OPTIONS,返回 204 并带上这些头。改完先用 nginx -t 测一遍配置,再 systemctl reload nginx 重载。

我自己的一次踩坑

我接过一个老接口,GET 全好,加了个 PUT 提交表单就崩。控制台没报 PUT 失败,而是说预检的 OPTIONS 返回里 Allow-Headers 没有 Authorization。我一开始盯着前端的 fetch 改了半天,后来才反应过来:浏览器根本没发出 PUT,卡在预检那一步。回到 Nginx 把 Authorization 补进 Allow-Headers,再加 OPTIONS 的 204 分支,一次就通了。从那以后我配跨域都先把方法和请求头列全,再核对一遍预检响应,而不是埋头改前端。线上配好后想确认浏览器实际收到的头对不对,可以用 HTTP 响应头查看工具 直接拉出来比对。

小结

跨域报错的本质,是服务端没在响应里声明放行。记住三件事就够用:Allow-Origin 决定谁能读,带凭证就不能用 *;非简单请求要先过预检 OPTIONS,Allow-Methods 和 Allow-Headers 得把你用到的方法和请求头列全;Nginx 里别漏 always。把这几个头配对,红字自然就消失了。


Made by Toolora · Updated 2026-06-13