Node.js + Express 入门实战笔记-03-CORS

CORS 跨域笔记


1. 同源策略

浏览器的同源策略要求下面三项全部相同才算同源。

示例: React 跑在 http://localhost:10086,API 在 http://localhost:8080

你的例子
协议 都是 http
域名 都是 localhost
端口 100868080跨域

所以 React(10086)请求 API(8080)属于跨域请求

补充: 跨域 ≠ 一定报错。浏览器会发请求,但若响应里没有合适的 CORS 头,浏览器会拦截 JS 读取响应,控制台才报 CORS 错。Postman、curl 不是浏览器,不受 CORS 限制,所以 Postman 能通、前端 fetch 报错,这是正常现象。


2. CORS 是什么

同源策略是浏览器的安全规则;CORS 是服务器通过响应头告诉浏览器「这个跨域请求我允许」的机制。

服务器要返回类似这样的头(开发环境常见写法):

复制代码
Access-Control-Allow-Origin: http://localhost:10086
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type

POST 带 Content-Type: application/json 时,浏览器还会先发一次 OPTIONS 预检请求,服务器也要正确响应,否则照样失败。

常用响应头说明

1. Access-Control-Allow-Origin

作用: 告诉浏览器「这个跨域响应允许被哪个页面读」。

  • * vs 具体域名: 带 Cookie / credentials: 'include' 时不能用 *,必须回显具体 origin。

  • 安全: 不能无脑 origin || '*' 反射任意 Origin(会被恶意站点利用);应做白名单校验。

javascript 复制代码
const allowed = ['https://a.com', 'https://b.com'];
if (allowed.includes(origin)) {
  res.set('Access-Control-Allow-Origin', origin);
}
2. Vary: Origin

作用: 告诉 CDN/缓存:响应内容随 Origin 请求头变化,不能对所有用户共用同一份缓存。

  • Origin: https://a.com → 缓存的 Allow-Origin: https://a.com

  • Origin: https://b.com → 应走另一份缓存(或缓存未命中再请求源站)

  • 不配的风险: A 站拿到的 CORS 头可能被缓存后给 B 站用,导致跨域失败或安全问题

提示: 只有在你动态设置 Allow-Origin(按 origin 回显)且前面有 CDN/代理缓存时,才强烈建议加 Vary: Origin。不是每个项目都必配(无 CDN/无缓存可略)。

3. Access-Control-Allow-Credentials

核心用途就一类:前后端分离且域名不同,还要用浏览器 Cookie 做登录态。

作用: 允许跨域请求携带 Cookie、 TLS 客户端证书(很少见)、HTTP 基本认证(浏览器弹窗那种,很少见)等凭证。

  • 前端必须配合: fetch(..., { credentials: 'include' }) 或 axios withCredentials: true

  • 互斥规则: Allow-Origin: * + Allow-Credentials: true → 浏览器直接拒绝

  • 若你写的是具体 origin(如 https://a.com),可以和 Credentials: true 一起用

提示: 登录态跨域、Cookie 跨子域失败时,检查这一对配置。

4. Access-Control-Allow-Methods

作用: 预检 OPTIONS 时告诉浏览器允许哪些方法。

  • 常见值: GET, POST, PUT, DELETE

  • 非简单请求(如自定义头、PUT、Content-Type: application/json)会先发 OPTIONS

提示: POST 带 JSON 会触发预检,OPTIONS 要返回这个头;纯 GET 简单请求往往不需要。

预检流程示例:

  1. 前端跨域 POST,并带 AuthorizationContent-Type: application/json

  2. 浏览器判断:非简单请求 → 先发 OPTIONS 预检

  3. 服务端 OPTIONS 响应要包含:Access-Control-Allow-Headers: Content-Type, Authorization

  4. 预检通过后,浏览器才发真正的 POST(带 Authorization)

为啥要 Content-Type?

  • Content-Type: application/json 不属于「简单 Content-Type」

  • 会触发预检,必须在 Allow-Headers 里声明

为啥要 Authorization?

  • Authorization 不是简单请求头

  • 前端带 Token 时,预检必须声明允许

5. Access-Control-Allow-Headers

作用: 预检时声明客户端(浏览器)可以带哪些非简单请求头。

  • 常见值: Content-Type, Authorization, X-Requested-With

  • 前端加了自定义头(如 X-Token)却没写在这里 → 预检失败

提示: 出现 Request header field xxx is not allowed 时补这个头。

6. Access-Control-Max-Age

作用: 浏览器缓存预检结果,减少重复 OPTIONS。

  • 86400 = 24 小时

  • 开发环境可设小一点便于调试;生产可设大

提示: 少发预检、提升性能。

7. Access-Control-Expose-Headers

默认行为: JS 只能读少数「安全」响应头(如 Content-Type)。

作用: 让前端能读自定义头,如 Content-Disposition(下载文件名)、Content-Length

  • 典型场景: 文件下载、分页总数在自定义头里

提示: 前端 response.headers.get('Content-Disposition')null 时,要加这个头。

后端完整 CORS 中间件示例

javascript 复制代码
app.use((req, res, next) => {
  res.set({
    "Access-Control-Allow-Origin": "http://192.168.88.119:10086/", // 允许哪个前端来源访问
    "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", // 允许哪些 HTTP 方法
    "Access-Control-Allow-Headers": "Content-Type, Authorization", // 允许哪些请求头
    "Access-Control-Allow-Credentials": "true", // 允许跨域带 Cookie / 凭证
    "Access-Control-Max-Age": "86400", // 预检 OPTIONS 结果缓存 24 小时,少发预检
    "Access-Control-Expose-Headers": "Content-Disposition, Content-Length", // 允许浏览器访问哪些响应头
  });

  if (req.method === "OPTIONS") {
    return res.status(204).send();
  }
  next();
});

3. 本项目的实际地址

局域网 IP http://192.168.88.119
后端端口 8080
接口路径示例 /api/todos
完整 API 地址 http://192.168.88.119:8080/api/todos

前后端端口号不一致,需要解决跨域。有两种常见方案:


4. 方案一:后端配置跨域头

javascript 复制代码
const allowedOrigins = [
  // ✅ 生产写法:按环境变量白名单来
  // "https://你的前端域名.com",
  // "https://www.你的前端域名.com",
  "http://localhost:10086",
  "http://192.168.88.119:10086",
];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    // 允许哪个前端来源访问
    res.set("Access-Control-Allow-Origin", origin);
    // 否则 Nginx、Cloudflare 等可能缓存错 origin 的响应,线上会出现「偶发 CORS 失败」。
    // 防止 CDN 把 A 域名的 CORS 响应缓存给 B 域名
    res.set("Vary", "Origin");
    // 允许跨域带 Cookie / 凭证
    res.set("Access-Control-Allow-Credentials", "true");
  }
  next();
});

5. 方案二:前端配置 Proxy 代理

前端配置 proxy 代理,不需要后端配置跨域头。

devServer 代理配置

javascript 复制代码
devServer: {
  proxy: {
    '/proxy': {
      // target: 'http://localhost:8080',
      target: 'http://192.168.88.119:8080',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/proxy/, ''),
    },
  },
},

前端请求示例

javascript 复制代码
const fetchData = () => {
  // const url = 'http://192.168.88.119:8080/api/todos'; // 需要后端配置跨域头,否则会报错
  const url = "/proxy/api/todos"; // 前端走代理,不需要后端配置跨域头

  fetch(url)
    .then((response) => response.json())
    .then((data) => console.log(data))
    .catch((error) => console.error("Error:", error));
};