Node系列 · Express:跨域(CORS)
前后端分离项目最常遇到的报错就是 CORS——浏览器拒绝跨域响应。本章讲清楚 CORS 的工作原理、Express 配置、不同场景的处理方式。
一、跨域问题的本质
text
页面:http://localhost:5173
接口:http://localhost:3000/api/users协议相同、域名相同、端口不同——不同源,浏览器拦截响应。
bash
$ curl http://localhost:3000/api/users
[200 OK] [{"id":1}, {"id":2}]
# curl 直接成功,浏览器失败
# 浏览器 DevTools:
# Access to fetch ... has been blocked by CORS policycurl 不受浏览器同源策略约束——只有浏览器才会拦截。
二、CORS 工作原理
CORS 通过 HTTP 响应头声明"允许谁访问":
http
HTTP/1.1 200 OK
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-Allow-Credentials: true
Access-Control-Max-Age: 864002.1 简单请求 vs 预检请求
浏览器按请求类型分两种:
| 类型 | 触发条件 |
|---|---|
| 简单请求 | GET / HEAD / POST 且不带自定义头 + Content-Type 是 application/x-www-form-urlencoded / multipart/form-data / text/plain |
| 预检请求 | 其他所有情况(PUT / DELETE / 自定义头 / application/json 等) |
TIP
前端用 fetch / axios 发送 application/json 的 POST 请求时,会先发 OPTIONS 预检。这是 CORS 配置错误的常见原因。
三、CORS 关键响应头
| 响应头 | 作用 |
|---|---|
Access-Control-Allow-Origin | 允许的源(* 或具体域名) |
Access-Control-Allow-Methods | 允许的 HTTP 方法 |
Access-Control-Allow-Headers | 允许的请求头 |
Access-Control-Allow-Credentials | 是否允许携带 Cookie |
Access-Control-Max-Age | 预检结果缓存秒数 |
Access-Control-Expose-Headers | 浏览器可读取的响应头白名单 |
四、简单请求处理
简单请求浏览器直接发,服务器响应头加 Access-Control-Allow-Origin 即可:
javascript
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
next();
});
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'Alice' }]);
});4.1 通配符 *
javascript
res.setHeader('Access-Control-Allow-Origin', '*'); // 允许所有源WARNING
带 Cookie 时不能用 *——Access-Control-Allow-Credentials: true + Access-Control-Allow-Origin: * 是非法组合,浏览器会拒绝。
五、预检请求处理
非简单请求浏览器先发 OPTIONS 探测:
text
OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization服务器必须响应这个 OPTIONS 请求:
javascript
app.options('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400'); // 缓存 24 小时
res.sendStatus(204);
});5.1 Access-Control-Max-Age
预检结果会被浏览器缓存:
- 默认:5 秒(Chrome)
- 设
Access-Control-Max-Age: 86400:缓存 24 小时 - 期间同源同方法同头的请求不再触发预检
减少 OPTIONS 请求对服务器的压力。
六、携带 Cookie / 认证信息
javascript
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true'); // 允许 Cookie
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});客户端要配合:
javascript
// fetch
fetch('https://api.example.com/users', {
credentials: 'include', // 携带 Cookie
});
// axios
axios.get('https://api.example.com/users', {
withCredentials: true,
});
// XMLHttpRequest
xhr.withCredentials = true;6.1 三条铁律
| 条件 | 必须 |
|---|---|
服务端开 Allow-Credentials: true | Origin 不能为 *,必须具体域名 |
客户端用 credentials: 'include' | 浏览器才会带上 Cookie |
浏览器看到 Allow-Credentials: true | 严格匹配 Origin,不允许通配符 |
七、动态 Origin 白名单
生产环境通常按需放行多个域名:
javascript
const ALLOWED_ORIGINS = [
'https://app.example.com',
'https://admin.example.com',
'http://localhost:5173', // 开发环境
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin'); // 关键:防止 CDN 缓存跨域头
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});TIP
Vary: Origin 必须加——当多个 Origin 共享缓存时,告诉 CDN / 反向代理"按 Origin 区分缓存"。
八、安全考虑
| 风险 | 防护 |
|---|---|
| Origin 反射漏洞 | 不要直接 Access-Control-Allow-Origin: req.headers.origin;用白名单校验 |
| 携带 Cookie 跨站攻击 | Allow-Credentials: true + 严格 Origin 白名单 |
| METHOD 反射 | 不用 Allow-Methods: req.headers['access-control-request-method'] |
| 协议降级 | 生产强制 HTTPS(防 Origin 伪造) |
DANGER
Origin 反射是 CORS 最严重的安全漏洞:
javascript
// ❌ 致命错误:任何网站都能跨域调你的 API
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
// ✅ 正确:白名单校验
const origin = req.headers.origin;
if (WHITELIST.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}九、最佳实践
| 场景 | 推荐 |
|---|---|
| 开发环境 | 用 cors 中间件一键配置(详见 cors 中间件) |
| 生产环境 | 动态 Origin 白名单 + Vary: Origin |
| 携带 Cookie | Allow-Credentials: true + 具体 Origin |
| 预检缓存 | Max-Age: 86400(24 小时) |
| 安全 | 永远不要反射 Origin / Methods |
十、小结
- CORS 通过响应头声明跨域权限;浏览器自动处理预检和简单请求
- 简单请求:直接发,服务器加
Access-Control-Allow-Origin即可 - 预检请求:浏览器先发 OPTIONS 探测,服务器必须响应
- 携带 Cookie 必须三件套:
Allow-Credentials: true+ 具体 Origin + 客户端credentials: 'include' - 永远不要反射 Origin,必须用白名单校验
- 多个 Origin 时加
Vary: Origin防止 CDN 缓存串
