CORS 跨域资源共享详解(含最佳实践)
适合读者 / 你将学到 / 阅读时长
- 适合读者:需要调用跨域接口的前端/Node 工程师、网关/运维同学
- 你将学到:同源与 CORS 的关系、简单请求与预检、凭据携带规则、缓存与 Vary、常见服务器配置
- 阅读时长:15 分钟
核心概念
- 同源策略(Same-Origin Policy):协议 + 域名 + 端口完全一致才是同源。
- CORS:一种服务器声明“哪些源可访问我资源”的机制,基于 HTTP 头。
简单请求 vs 预检请求
- 简单请求(Simple Request):直接发请求,浏览器仅在响应阶段检查 CORS。需满足:
- 方法:GET/HEAD/POST
- 头:仅限 Accept、Accept-Language、Content-Language、Content-Type(且值为 application/x-www-form-urlencoded、multipart/form-data 或 text/plain)
- 无自定义头、无复杂 Content-Type
- 预检请求(Preflight):其他情况,浏览器会先发
OPTIONS请求询问服务端是否允许,服务端需返回允许的源/方法/头等,随后才发送真正请求。
关键响应头
Access-Control-Allow-Origin: 允许的来源,值可以是具体域名或*。Access-Control-Allow-Methods: 允许的方法列表,如GET,POST,PUT,DELETE。Access-Control-Allow-Headers: 允许的请求头(当请求包含自定义头时返回)。Access-Control-Allow-Credentials: 是否允许携带凭据(Cookie、Authorization 等),值为true/false。Access-Control-Expose-Headers: 暴露给浏览器 JS 可读的响应头。Access-Control-Max-Age: 预检结果的缓存时间(秒)。
注意:当 Allow-Credentials: true 时,Allow-Origin 不能为 *,必须为明确的源。
凭据与 Cookie
- 前端需在请求中显式开启:
fetch(url, { credentials: 'include' })或axios.defaults.withCredentials = true。 - 服务器需返回:
Access-Control-Allow-Credentials: true且Access-Control-Allow-Origin为具体域名。 - 跨站 Cookie 需要
SameSite=None; Secure,否则不会随跨站请求发送,也无法被设置。
缓存与 Vary
- 使用
Access-Control-Max-Age减少频繁预检,例如600(10 分钟)。 - 当你按“来源”动态返回
Allow-Origin时,应加Vary: Origin,以免 CDN/缓存污染其他来源。
常见服务端配置
Nginx(推荐在网关层统一处理)
nginx
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $http_origin always;
add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always; # 如需携带凭据
add_header 'Access-Control-Max-Age' 600 always; # 预检缓存
add_header 'Vary' 'Origin';
return 204;
}
add_header 'Access-Control-Allow-Origin' $http_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Vary' 'Origin';
proxy_pass http://backend;
}提示:若不需要携带凭据,可将
Allow-Credentials去掉并把Allow-Origin固定为*。
Node(Express + cors 中间件)
js
// Express + cors 中间件(CommonJS 示例)
const express = require('express');
const cors = require('cors');
const app = express();
app.use(
cors({
origin: [/^https?:\/\/localhost:\d+$/, 'https://www.cmwrun.com'],
credentials: true, // 需要携带 Cookie/Authorization 时开启
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
maxAge: 600,
})
);
app.get('/api/data', (req, res) => {
res.json({ ok: true });
});
app.listen(3000);Koa(手写头部示例)
js
// Koa 简易示例(按需调整到应用入口)
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
const origin = ctx.request.headers.origin;
const allowList = new Set(['https://www.cmwrun.com', 'http://localhost:5173']);
if (origin && allowList.has(origin)) {
ctx.set('Access-Control-Allow-Origin', origin);
ctx.set('Access-Control-Allow-Credentials', 'true');
ctx.set('Vary', 'Origin');
}
ctx.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
ctx.set('Access-Control-Max-Age', '600');
if (ctx.method === 'OPTIONS') ctx.status = 204; else await next();
});常见错误与排查
- 返回了
Access-Control-Allow-Credentials: true但Allow-Origin: *→ 违规则组合,浏览器直接拦截。 - 忘记
Vary: Origin导致 CDN 缓存污染,A 源缓存命中到 B 源请求。 - 只配了
Allow-Headers,却在预检时漏了Authorization等自定义头。 - 预检返回非 2xx/204 或者漏配
OPTIONS路由(Node 常见)。 - 跨站 Cookie 未设置
SameSite=None; Secure,导致怎么都带不上 Cookie。
快速自查清单
- 设置
Allow-Origin为明确域名或回显$http_origin,并配Vary: Origin - 需要凭据?→ 前端
withCredentials/credentials=include+ 服务端Allow-Credentials: true,且Allow-Origin不为* - 确认
OPTIONS预检能 204 返回,并包含允许的方法与头 - 适度设置
Max-Age,减少预检次数
本地调试建议
bash
# 使用 curl 观察预检
curl -i -X OPTIONS \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization, Content-Type" \
https://api.example.com/resource
# 观察实际请求的 CORS 响应头
curl -i -H "Origin: http://localhost:5173" https://api.example.com/resource参考:Fetch 标准(CORS 章节)、MDN CORS 指南、Nginx 文档、expressjs/cors