跳到主要内容

跨域资源共享(CORS)

跨域资源共享(CORS) 是一种机制,允许浏览器从不同域(协议、域名或端口)加载资源。它通过使用额外的 HTTP 头来告诉浏览器允许哪些资源可以被访问,以及哪些 HTTP 方法和头部可以被使用。

跨域的背景和同源策略

Web 为什么会出现跨站访问?

早期网页以静态页面为主,页面、图片、脚本通常都来自同一个站点。后来 Web 应用逐渐拆分成多套服务:

  • 前端页面部署在 https://app.example.com
  • API 服务部署在 https://api.example.com
  • 静态资源放在 CDN,例如 https://cdn.example.com
  • 第三方登录、支付、地图、埋点服务来自外部域名

这类架构是现代 Web 的常态。浏览器既要允许页面加载图片、脚本、样式等跨站资源,又要防止一个恶意页面读取用户在另一个站点中的隐私数据。因此浏览器需要一条清晰的安全边界: 跨站资源可以被使用到一定程度,但不能随意被 JavaScript 读取

同源策略(Same-Origin Policy)

浏览器内置了一项安全机制——同源策略(SOP),规定:一个网页只能自由读取来自"同一来源"的资源

"同源"需要三者完全一致:

维度示例
协议(Scheme)https://
域名(Host)app.example.com
端口(Port)443

以下均属于跨域

当前页面请求地址原因
https://app.example.comhttps://api.example.com子域名不同
https://app.example.comhttp://app.example.com协议不同
https://app.example.comhttps://app.example.com:8080端口不同
https://app.example.comhttps://other.com域名不同

为什么要有这个限制?

同源策略的核心目标不是让后端接口更难调用,而是防止“用户已经登录某站点”这件事被恶意页面利用。

若缺少同源策略,风险链路如下:

用户登录了 bank.com(Cookie 保存在浏览器)
用户打开了 evil.com
evil.com 的页面悄悄向 bank.com/transfer 发请求
浏览器自动带上 bank.com 的 Cookie
evil.com 可读取 bank.com 返回的账户余额、交易记录、个人资料

在这种模型下,只要用户访问了恶意网站,恶意网站就能以用户身份读取另一个网站的数据。同源策略用于阻断这类跨站读取行为,是浏览器最重要的安全基石之一, 它保护的是用户,而不是开发者

同源策略重点解决的是“跨站读取响应”的问题,但它不等于完整的跨站攻击防护。另一个相关风险是 CSRF(Cross-Site Request Forgery,跨站请求伪造):恶意页面诱导浏览器携带用户已有登录态,向目标站点发起转账、删除、修改配置等有副作用的请求。

两者的防护边界不同:

  • 同源策略主要限制浏览器中的 JavaScript 读取跨域响应
  • CSRF 关注的是恶意页面能否借用户身份 发起有副作用的请求
  • 即使浏览器因为 CORS 拦截了响应,服务端也可能已经收到了请求,所以写接口仍然需要 CSRF Token、SameSite Cookie、权限校验等保护
注意

同源策略限制的是浏览器中的 JavaScript 读取响应内容。请求本身可能已经发出,服务端也可能已经收到并处理,只是浏览器拒绝把响应交给 JS。curl、Postman 等非浏览器工具不受同源策略约束。


CORS 的工作原理

CORS(Cross-Origin Resource Sharing,跨源资源共享)是浏览器和服务器协商的一套机制,**用于让服务端声明哪些来源可以跨域读取资源 **。

CORS 协议本质上是浏览器向服务端发起授权确认:

请求页面来源: https://app.example.com
目标资源地址: https://api.example.com/data
授权目标: 是否允许该来源读取响应

服务端通过 Access-Control-* 响应头声明授权结果。响应头满足 CORS 规则时,浏览器才会把响应暴露给 JavaScript;否则浏览器会在控制台报告 CORS 错误。

CORS 拦截的是哪一步?

CORS 的限制点不总是在请求发送阶段。更准确的执行过程如下:

阶段是否可能发生说明
发送预检请求可能发生非简单请求会先发 OPTIONS,预检失败则不会发送真实请求
发送真实请求可能发生简单请求会直接发送;预检通过后也会发送真实请求
服务端处理请求可能发生服务端可能已经执行业务逻辑
JavaScript 读取响应受 CORS 控制响应头不符合 CORS 规则时,浏览器拒绝把响应内容暴露给 JS

定位 CORS 问题时,需要先区分两个层面:

  • 请求是否到达服务端:通过服务端访问日志、网关日志、DevTools Network 确认
  • 响应是否暴露给 JavaScript:通过响应中的 Access-Control-* 头确认

简单请求(Simple Request)

满足以下全部条件时,浏览器直接发请求,不做预检:

  • 方法为 GETPOSTHEAD 之一
  • 请求头仅包含安全头(AcceptContent-Type: text/plain|form 等)
  • 无自定义头

浏览器在请求中自动添加 Origin,服务端响应中若包含合法的 Access-Control-Allow-Origin,浏览器放行。

浏览器(app.example.com) 服务器(api.example.com)
│── GET /data ────────────────────→ │
│ Origin: https://app.example.com │
│ │
│ ←─ 200 OK ────────────────────── │
│ Access-Control-Allow-Origin: https://app.example.com
│ │
浏览器放行,JS 可读取响应

预检请求(Preflight Request)

当请求不满足"简单请求"条件时(如使用 PUT/DELETE、自定义头、Content-Type: application/json),浏览器先发一个 OPTIONS 预检请求,获得服务端许可后才发真实请求。

浏览器(app.example.com) 服务器(api.example.com)
│── OPTIONS /data ──────────────────→ │
│ Origin: https://app.example.com │
│ Access-Control-Request-Method: POST
│ Access-Control-Request-Headers: Authorization, Content-Type
│ │
│ ←─ 204 No Content ──────────────── │
│ Access-Control-Allow-Origin: https://app.example.com
│ Access-Control-Allow-Methods: POST
│ Access-Control-Allow-Headers: Authorization, Content-Type
│ │
│── POST /data ──────────────────────→ │ (真实请求)
│ ←─ 200 OK ──────────────────────── │
浏览器放行

关键响应头详解

Access-Control-Allow-Origin

声明允许哪些源访问。

Access-Control-Allow-Origin: * # 允许任意源(不能与凭证同用)
Access-Control-Allow-Origin: https://app.example.com # 只允许指定源
备注

只能填一个值,不能填多个域名。如需支持多域名,需在服务端动态判断 Origin 请求头并响应对应值。

Access-Control-Allow-Methods

声明预检通过后,允许的 HTTP 方法。

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

Access-Control-Allow-Headers

声明允许携带的自定义请求头。

Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
Access-Control-Allow-Headers: * # 允许任意请求头(无凭证时有效)

Access-Control-Expose-Headers

声明哪些响应头可以被浏览器中的 JS 读取。

Access-Control-Expose-Headers: X-Request-ID, X-Trace-ID, Content-Length

默认情况下,JS 只能读取以下 7 个 CORS 安全头(Safelisted Headers),其余全部被浏览器屏蔽:

  • Cache-Control
  • Content-Language
  • Content-Length
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

Access-Control-Allow-Credentials

声明是否允许请求携带凭证(Cookie、HTTP 认证、TLS 客户端证书)。

Access-Control-Allow-Credentials: true

Access-Control-Max-Age

预检结果缓存时间(秒),避免每次请求都发 OPTIONS。

Access-Control-Max-Age: 86400 # 缓存 24 小时

Timing-Allow-Origin

Resource Timing API 是浏览器提供的性能观测接口,用于读取页面加载资源时的耗时数据,例如 DNS 查询、TCP 连接、TLS 握手、请求发送、响应接收等阶段的时间。前端性能监控、首屏分析、CDN 质量分析通常会通过 performance.getEntriesByType('resource')PerformanceObserver 读取这些数据。

const entries = performance.getEntriesByType('resource');
for (const entry of entries) {
console.log(entry.name, entry.responseStart, entry.responseEnd);
}

对于跨域资源,浏览器默认会隐藏部分详细耗时字段,避免页面借性能时间推断跨站资源状态。Timing-Allow-Origin 用于声明哪些来源可以读取这些跨域资源的详细性能指标。

Timing-Allow-Origin: https://app.example.com
Timing-Allow-Origin: *

Timing-Allow-Origin 不是 CORS 放行头。即使响应中包含它,浏览器也不会因此允许 JavaScript 读取响应内容;跨域读取仍然取决于 Access-Control-Allow-Origin 等 CORS 响应头。


凭证请求(Credentials)的特殊限制

什么是凭证请求?

当前端使用以下方式发起请求时,称为凭证请求

// fetch
fetch('https://api.example.com/data', {credentials: 'include'})

// axios
axios.create({withCredentials: true})

// XMLHttpRequest
xhr.withCredentials = true

凭证请求会让浏览器在跨域请求中携带 Cookie、HTTP 认证信息等。

浏览器里有 Cookie ≠ 跨域请求会自动带上它。fetch 的默认凭证模式是 same-origin

模式同源请求跨域请求
same-origin(默认)带 Cookie不带 Cookie ❌
include带 Cookie带 Cookie
omit不带 Cookie ❌不带 Cookie ❌

credentials: 'include' 只表示浏览器允许本次跨域请求携带凭证,最终是否携带 Cookie 还要受 Cookie 自身属性限制。现代浏览器默认倾向于 SameSite=Lax,跨站 fetch / XHR 请求通常不会携带这类 Cookie。

Cookie 属性跨站 fetch / XHR典型用途
SameSite=Strict不发送高敏感站内会话
SameSite=Lax通常不发送默认会话 Cookie
SameSite=None可发送,但必须配合 Secure需要第三方或跨站访问的 Cookie

SameSite=None; Secure 允许 Cookie 出现在跨站 HTTPS 请求中,但也会扩大 CSRF 风险面。需要登录态的写接口仍应配合 CSRF Token、来源校验或其他业务权限校验。

凭证请求时,服务端的强制约束

当浏览器发现请求携带凭证,会对服务端响应做更严格的校验:

响应头普通跨域请求凭证跨域请求
Access-Control-Allow-Origin可以用 *必须指定具体域名,不能用 *
Access-Control-Allow-Headers可以用 *不能用 *,必须显式列出
Access-Control-Expose-Headers可以用 *不能用 *,必须显式列出
Access-Control-Allow-Credentials不需要必须为 true

为什么有这个限制?

* 代表"任意来源都可以用你的身份读取响应",这等同于彻底击穿同源策略的保护。规范设计者因此在有凭证的场景下禁止了通配符。


Access-Control-Expose-Headers 与 * 通配符的陷阱

问题场景

以下服务端配置在凭证模式下不会达到预期效果:

Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: * ← 凭证模式下,* 不是通配符

浏览器按规范处理:当请求是凭证请求时,Expose-Headers 中的 * 被当作字面头名 * 而非通配符,结果等同于没有暴露任何额外头。

JS 里依然只能读到 7 个安全头,其他头全部不可见:

const res = await fetch('https://api.example.com/data', {
credentials: 'include'
});

for (const [k, v] of res.headers.entries()) {
console.log(k, v);
// 只能看到: content-type, content-length 等安全头
// x-request-id、x-trace-id 等自定义头:读不到
}

根本原因

* 是否生效,由请求的凭证状态决定,而非响应头本身:

请求没有携带凭证(credentials: 'same-origin' 或 'omit')
→ * 被当作通配符 → 所有头可见

请求携带了凭证(credentials: 'include')
→ * 被当作字面字符串 → 只有安全头可见 ❌

正确做法

Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Content-Type, X-Request-ID, X-Trace-ID, Date

按需显式列出前端需要读取的响应头。

配置速查表

参数无凭证请求有凭证请求
Allow-Origin* 或具体域名必须具体域名
Allow-Credentials不需要必须 true
Allow-Headers* 或列表必须列表
Expose-Headers* 或列表必须列表

使用 curl 调试跨域

curl 可用于验证服务端的 CORS 响应头,但不会触发浏览器的同源策略,也不会执行浏览器侧的 CORS 判定。它的作用是模拟浏览器发送关键请求头,确认服务端响应是否符合预期。

curl 的适用边界:

  • curl 请求成功,不等于浏览器跨域访问成功
  • curl 响应头不符合 CORS 规则时,浏览器会拦截响应
  • curl 适合验证服务端 CORS 配置,不适合替代浏览器最终验收

调试简单请求

普通 GET 请求可通过 Origin 头模拟跨域来源:

curl -i 'https://api.example.com/data' \
-H 'Origin: https://app.example.com'

响应需要包含对应的 CORS 头:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-ID
Vary: Origin

无凭证公开接口可以返回 Access-Control-Allow-Origin: *;使用 credentials: 'include' 的请求必须返回具体 Origin,不能使用 *

调试预检请求:curl -X OPTIONS

预检请求由 OPTIONS 方法和以下请求头共同构成:

  • Origin:当前页面的来源
  • Access-Control-Request-Method:真实请求准备使用的方法
  • Access-Control-Request-Headers:真实请求准备携带的非简单请求头

以下前端请求会触发预检:

fetch('https://api.example.com/data', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer xxx'
},
body: JSON.stringify({name: 'demo'})
})

预检请求命令示例:

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

服务端预检响应示例:

HTTP/2 204
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Vary: Origin

预检请求头与服务端响应头的对应关系:

预检请求头服务端响应头要求
Origin: https://app.example.comAccess-Control-Allow-Origin必须匹配;凭证请求不能是 *
Access-Control-Request-Method: POSTAccess-Control-Allow-Methods必须包含 POST
Access-Control-Request-Headers: authorizationAccess-Control-Allow-Headers必须包含 Authorization,大小写不敏感
前端 credentials: 'include'Access-Control-Allow-Credentials必须是 true

预检通过时的 curl 输出

服务端正确配置了 CORS,响应中包含所有必要的 Access-Control-* 头:

$ curl -i -X OPTIONS 'https://api.example.com/data' \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type, authorization'
HTTP/2 204
access-control-allow-origin: https://app.example.com
access-control-allow-credentials: true
access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS
access-control-allow-headers: Content-Type, Authorization
access-control-max-age: 86400
vary: Origin

浏览器同时验证以下四项,全部通过才放行后续真实请求:

  • Access-Control-Allow-Origin 与请求中的 Origin 完全一致
  • Access-Control-Allow-Methods 包含真实请求使用的 POST
  • Access-Control-Allow-Headers 包含 AuthorizationContent-Type
  • Access-Control-Allow-Credentials: true 满足 credentials: 'include' 的要求

预检不通过时的响应与报错

以下是常见的失败场景:每种场景给出 curl 看到的服务端响应,以及浏览器 Console 对应的报错文本:

场景 A:服务端未配置 CORS,OPTIONS 请求返回 403

HTTP/2 403
content-type: text/plain; charset=utf-8

Forbidden

响应中完全没有 Access-Control-Allow-* 头。浏览器报错:

Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: No 'Access-Control-Allow-Origin' header is present.

场景 B:Origin 不在白名单,服务端不返回 Access-Control-Allow-Origin

HTTP/2 200
content-type: text/plain

# 响应体可能为空或有内容,但缺少:
# access-control-allow-origin: https://app.example.com

浏览器报错与场景 A 相同:No 'Access-Control-Allow-Origin' header is present

场景 C:请求方法不在 Access-Control-Allow-Methods

HTTP/2 204
access-control-allow-origin: https://app.example.com
access-control-allow-headers: Content-Type, Authorization
# 缺少 access-control-allow-methods,或其值不包含 POST

浏览器报错:

Method POST is not allowed by Access-Control-Allow-Methods in preflight response.

场景 D:自定义请求头不在 Access-Control-Allow-Headers

HTTP/2 204
access-control-allow-origin: https://app.example.com
access-control-allow-methods: GET, POST, OPTIONS
access-control-allow-headers: Content-Type
# Authorization 不在 Allow-Headers 中

浏览器报错:

Request header field authorization is not allowed by Access-Control-Allow-Headers
in preflight response.

场景 E:凭证请求但缺少 Access-Control-Allow-Credentials: true

HTTP/2 204
access-control-allow-origin: https://app.example.com
access-control-allow-methods: GET, POST, OPTIONS
access-control-allow-headers: Content-Type, Authorization
# 缺少 access-control-allow-credentials: true

浏览器报错:

The value of the 'Access-Control-Allow-Credentials' header in the response is ''
which must be 'true' when the request's credentials mode is 'include'.
排查思路

curl -i 能确认服务端返回了哪些 Access-Control-* 头;浏览器 Console 的报错信息才能指出具体哪一项校验失败。推荐先用 curl 确认服务端响应,再对照 DevTools Network / Console 面板定位根本原因。

调试真实请求

预检成功后,真实请求的响应仍然需要携带必要的 CORS 头。若服务端只为 OPTIONS 响应添加 CORS 头,而真实 POST / GET 响应缺少相关头,浏览器仍会拦截响应。

curl -i -X POST 'https://api.example.com/data' \
-H 'Origin: https://app.example.com' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer xxx' \
--data '{"name":"demo"}'

真实响应需要包含:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-ID

curl 的调试边界

curl 只能呈现 HTTP 层结果,不能完整复现浏览器行为:

  • 不会自动执行浏览器的 CORS 校验
  • 不会自动区分简单请求和非简单请求
  • 不会自动执行预检再执行真实请求
  • 不会模拟 fetchcredentials 默认值
  • 不会受 Cookie 的 SameSiteSecure、第三方 Cookie 策略影响
  • 不会验证 JS 是否能读取响应头,Expose-Headers 仍需在浏览器里确认

实践中可先通过 curl 定位服务端响应头问题,再通过浏览器 DevTools 验证最终行为。

常见配置场景

以下示例均以 https://app.example.com(前端)调用 https://api.example.com(后端)为例。


场景一:公开 API,无需登录态

适用于:CDN 静态资源、完全开放的数据接口。

# 服务端响应头
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Expose-Headers: *

* 通配符完全有效,JS 可读取所有暴露头。


适用于:前端与后端在不同子域,请求需要带认证 Cookie。

# 服务端响应头
Access-Control-Allow-Origin: https://app.example.com ← 必须指定具体域名
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
Access-Control-Expose-Headers: Content-Type, X-Request-ID, X-Trace-ID ← 必须显式列出
Access-Control-Max-Age: 86400

前端:

fetch('https://api.example.com/data', {
credentials: 'include', // 必须显式声明才会带 Cookie
headers: {'Content-Type': 'application/json'}
})

场景三:支持多个前端域名

服务端动态判断 Origin 并响应:

// Node.js / Express 示例
const ALLOWED_ORIGINS = [
'https://app.example.com',
'https://admin.example.com',
'https://staging.example.com',
];

app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin); // 动态写回请求方的 Origin
res.setHeader('Vary', 'Origin'); // 告知缓存此响应因 Origin 而不同
}
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Expose-Headers', 'X-Request-ID, X-Trace-ID');

if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
警告

动态设置时必须加 Vary: Origin,否则 CDN / 代理缓存会把某个 Origin 的响应头返回给其他 Origin,导致 CORS 失败。


场景四:本地开发(localhost)

Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Expose-Headers: Content-Type, X-Request-ID
提示

localhost 不能用 *credentials: true 的组合,和生产环境规则一致。


场景五:接口重定向到对象存储或下载地址

下载接口常见流程是前端先请求业务 API,业务 API 再通过 302 / 307 跳转到对象存储、CDN 或预签名下载地址。浏览器会继续对最终地址执行 CORS 校验,最终资源的响应头也必须满足当前页面的跨域读取规则。

常见失败原因:

  • 对象存储不处理 OPTIONS 预检请求,导致真实下载请求不会发出
  • 初始请求携带 Cookie、Authorization 或自定义头,触发预检后被最终资源服务拒绝

若最终地址是公开资源或预签名 URL,前端应尽量让请求保持为无凭证的简单请求:

const text = await fetch(downloadUrl, {
credentials: 'omit'
}).then((res) => res.text());

若必须在 JS 中读取响应内容,对象存储或 CDN 仍需配置对应的 CORS 规则。若只是让浏览器下载文件,优先使用顶层跳转或普通链接,由浏览器导航流程处理下载,避免把文件下载建模成带凭证的 AJAX 请求。


常见错误排查

No 'Access-Control-Allow-Origin' header

Access to fetch at 'https://api.example.com' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present

原因:服务端未配置 CORS,或 OPTIONS 预检请求未正确处理。
验证命令

curl -X OPTIONS https://api.example.com/data \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' -v
# 响应中应包含 Access-Control-Allow-Origin

credentials mode is 'include' 但无法通过

The value of the 'Access-Control-Allow-Credentials' header in the response is ''
which must be 'true' when the request's credentials mode is 'include'.

原因:前端用了 credentials: 'include' 但服务端未返回 Access-Control-Allow-Credentials: true
修复:服务端添加 Access-Control-Allow-Credentials: true


wildcard in Allow-Origin cannot be used with credentials

The value of the 'Access-Control-Allow-Origin' header must not be '*'
when the request's credentials mode is 'include'.

原因:凭证请求下服务端返回了 Access-Control-Allow-Origin: *
修复:改为具体域名,如 Access-Control-Allow-Origin: https://app.example.com


自定义响应头在 JS 中读不到

现象:DevTools Network 能看到 X-Request-ID 响应头,但 response.headers.get('x-request-id') 返回 null
原因:服务端未在 Access-Control-Expose-Headers 中声明该头。
修复:将需要的头加入 Access-Control-Expose-Headers 列表。


Expose-Headers: * 配了但 JS 仍读不到

现象:服务端返回 Access-Control-Expose-Headers: *,JS 仍然只能读到安全头。
原因:请求携带了凭证(credentials: 'include'),此时 * 是字面量而非通配符(见 Expose-Headers 通配符陷阱)。
修复:改为显式列出需要暴露的头名称。


多域名场景下 CDN 缓存串扰

现象app.example.com 有时收到 Access-Control-Allow-Origin: https://admin.example.com,导致 CORS 失败。
原因:动态设置 Allow-Origin 时没有添加 Vary: Origin,CDN 缓存了错误的响应头。
修复:服务端动态响应时务必加 Vary: Origin


现象:前端已配置 credentials: 'include'withCredentials: true,但请求头中没有目标站点的 Cookie。
原因:Cookie 可能被 SameSiteSecureDomainPath 或浏览器第三方 Cookie 策略拦截。跨站 fetch / XHR 通常需要 SameSite=None; Secure
修复:先在 DevTools Application / Storage 中确认 Cookie 属性,再检查请求是否为 HTTPS、目标域名是否匹配 Cookie 的 Domain / Path,最后确认服务端 CORS 是否允许凭证请求。


重定向到对象存储后 CORS 失败

现象:业务接口返回 302 / 307 后,浏览器访问最终下载地址时报 CORS 错误。
原因:CORS 校验会作用于最终资源地址;对象存储或 CDN 未处理预检请求,或未返回允许当前 Origin 的 CORS 响应头。
修复:公开或预签名下载地址优先使用无凭证简单请求;需要在 JS 中读取响应内容时,为对象存储或 CDN 配置对应的 CORS 规则。


参考文档