CORS 报错(“No 'Access-Control-Allow-Origin' header is present”)的意思是:某个源上的网页(比如 http://localhost:5173)调用了另一个源上的 API,而响应没有声明允许这个源,所以浏览器拒绝让页面读取响应。解决办法在服务器端:在 Access-Control-Allow-Origin 中返回页面的源,并用你接受的方法和请求头来响应浏览器的预检 OPTIONS 请求。在 Express 中,这只需要几行中间件代码,或者使用 cors 包的 app.use(cors())。
响应是商品列表,在它的“标头”标签页中可以看到 access-control-allow-origin: https://app.example.com:这个源上的页面可以读取它。现在发送 OPTIONS /api/products/1,带上请求头 Origin: https://app.example.com 和 Access-Control-Request-Method: PUT。这就是浏览器在发送 PUT 之前发出的预检请求,返回的是 204,带有允许的方法和请求头,没有响应体。
浏览器检查什么
同源页面什么都不需要。对于跨源请求,浏览器会读取以下响应头:
| 响应头 | 含义 |
|---|---|
Access-Control-Allow-Origin | 允许读取响应的那一个源,或者 * 表示任意源 |
Access-Control-Allow-Methods | 实际请求中允许使用的方法(仅预检) |
Access-Control-Allow-Headers | 页面可以发送的请求头,比如 Authorization(仅预检) |
Access-Control-Allow-Credentials | 为 true 时,页面可以发送 cookie 并读取响应 |
Access-Control-Max-Age | 浏览器可以缓存预检响应的秒数 |
Vary: Origin | 告诉缓存:响应取决于 Origin 请求头 |
简单的 GET 或表单式的 POST 会直接发送到服务器,浏览器再检查响应中的 Access-Control-Allow-Origin。服务器依然会执行这个请求;CORS 只决定页面能不能看到结果。带 JSON 请求体、Authorization 请求头,或者使用 PUT、DELETE 这类方法的请求,会先发送一个预检 OPTIONS 请求,只有预检响应允许时,才会发送实际请求。所以 CORS 中间件必须在任何路由或身份验证检查之前响应 OPTIONS。
允许多个源,并携带凭据
Access-Control-Allow-Origin 只能填一个源或 *,不能填列表。要允许多个源,就把请求的 Origin 请求头和允许列表进行比较,匹配时原样返回:
第一个请求没有 Origin 请求头,就像来自 curl 或同源页面的调用,所以它得到 {"origin":null},也没有 CORS 响应头。发送 GET /api/me 并带上请求头 Origin: http://localhost:5173,“标头”标签页会显示原样返回的这个源,以及 access-control-allow-credentials: true。把请求头改成 Origin: https://evil.example,两个 CORS 响应头就都消失了:服务器仍然会响应,但浏览器会阻止页面读取它。Access-Control-Allow-Credentials: true 允许页面发送 cookie(使用 fetch(url, { credentials: 'include' })),而浏览器会拒绝它和 * 同时出现,所以携带凭据时必须指定确切的源。
cors 包
cors 包通过选项完成同样的工作。用 npm install cors 安装:
const cors = require('cors');
// Any origin, no cookies: fine for a public read-only API.
app.use(cors());
// Only your frontend, with cookies.
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
// One route only.
app.get('/api/public', cors(), handler);
不带选项调用时,cors() 会发送 Access-Control-Allow-Origin: * 并响应所有预检请求。只对不需要登录的公开数据使用 *。
常见错误
在路由或身份验证中间件之后才添加 CORS。 预检 OPTIONS 请求不携带 Authorization 请求头,所以放在前面的身份验证检查会用 401 响应它,浏览器就报出 CORS 错误。请先注册 CORS 中间件。
携带凭据时设置 *。 浏览器会拒绝这个响应。请改为原样返回具体的源。
试图在前端解决 CORS。 像 mode: 'no-cors' 这样的请求选项并不能解除限制,它只会给你一个无法读取的不透明响应。只有服务器的响应头才能解决问题。
关于 app.use() 和中间件的执行顺序,请参阅 Express 中间件。
常见问题
为什么我的 API 在 Postman 或 curl 中能用,在浏览器中却不行?
只有浏览器会执行 CORS 限制。Postman、curl 和其他服务器都会忽略 CORS 响应头,所以无论如何都能读取响应。而浏览器会拒绝把响应交给来自另一个源的页面脚本,除非服务器允许了那个源。
CORS 是保护服务器的安全机制吗?
它保护的是用户,不是你的服务器。它阻止恶意页面带着用户的 cookie 读取你的 API 响应。任何人仍然可以从脚本或服务器调用你的 API,所以身份验证和权限控制仍然是你要负责的事。
什么情况算是不同的源?
源由协议、主机和端口共同组成。http://localhost:5173 和 http://localhost:3000 是不同的源,因为端口不同;https://example.com 和 https://api.example.com 则是主机不同。
开发时能避开 CORS 吗?
可以,让请求变成同源就行:配置你的前端开发服务器(例如 Vite 的 server.proxy),把 /api 转发给 Express 服务器。这样浏览器只和一个源通信,就不需要任何 CORS 响应头了。