Menu

Express 跨域 CORS:解决 CORS 报错(用或不用 cors 包)

CORS 报错的意思是:响应没有允许页面所在的源,所以浏览器阻止了页面读取你的 API 响应。要在服务器端解决:发送 Access-Control-Allow-Origin,并响应预检 OPTIONS 请求,可以手写,也可以用 cors 包。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 响应头了。

Coddy 编程语言插图

和 Coddy 一起学编程

开始