Skip to content

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 policy

curl 不受浏览器同源策略约束——只有浏览器才会拦截

二、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: 86400

2.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 请求对服务器的压力。

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: trueOrigin 不能*,必须具体域名
客户端用 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
携带 CookieAllow-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 缓存串