Skip to content
CORS 预检
浏览器在发送跨域请求时先发起 OPTIONS 请求的机制、触发条件与服务器配置。
概述
在浏览器的 Network 面板中观察跨域 API 调用时,会先看到一个 OPTIONS 请求,然后才是 GET、POST 等实际请求。这个额外的 OPTIONS 请求就是 CORS 预检(Preflight)。
基本概念
浏览器的同源策略(Same-Origin Policy)只允许脚本读取同源(协议、域名、端口完全相同)的响应。任何一项不同即构成跨域,请求虽然能被发出,但响应体会被浏览器拦截。
同源策略主要限制三类行为:
- 读取跨域
iframe的 DOM - 读取跨域请求的响应体
- 访问跨域的 Cookie、LocalStorage、IndexedDB
CORS(Cross-Origin Resource Sharing)是一套由服务器通过特定响应头声明允许哪些源访问本域资源的机制,是对同源策略的受控放宽。预检就是这套机制中的安全关卡:在实际请求发出之前,浏览器先发送一个 OPTIONS 请求向服务器确认是否放行。
工作原理
简单请求与预检触发条件
只有“非简单请求”才会触发 OPTIONS 预检。简单请求必须同时满足三个条件:
- 请求方法是
GET、HEAD或POST。 - 请求头(除浏览器自动附加的 CORS 安全列表头之外)只能包含:
AcceptAccept-LanguageContent-LanguageContent-Type,且值仅限于:application/x-www-form-urlencodedmultipart/form-datatext/plain
Range
- 请求不携带
ReadableStream对象;如果使用XMLHttpRequest,未注册upload事件监听器。
任意一条不满足,浏览器就会在实际请求前自动发送 OPTIONS 询问。
常见的触发组合是 POST 配合 Content-Type: application/json 和 Authorization: Bearer xxx。application/json 不在允许的 Content-Type 列表中,Authorization 属于自定义头,这两个因素叠加必然触发预检。
预检请求与响应
浏览器发出的 OPTIONS 请求类似:
http
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-typeOrigin—— 请求来源Access-Control-Request-Method—— 实际请求将使用的 HTTP 方法Access-Control-Request-Headers—— 实际请求将携带的自定义头部列表
服务器许可后可以返回:
http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400响应头说明:
| 响应头 | 含义 |
|---|---|
Access-Control-Allow-Origin | 允许的源。若请求携带凭据(credentials),此字段不能设为 * |
Access-Control-Allow-Methods | 允许的 HTTP 方法列表 |
Access-Control-Allow-Headers | 允许的自定义请求头列表 |
Access-Control-Max-Age | 浏览器可缓存该预检结果的时长(秒) |
预检失败时,浏览器控制台会输出类似以下错误:
Access to fetch at 'https://api.example.com/data' from origin 'https://myapp.com'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: No 'Access-Control-Allow-Origin' header is present on
the requested resource.如果 OPTIONS 请求本身返回了非 2xx 状态码(如 401 或 500),浏览器同样会阻止后续的实际请求。排查时需检查 Network 面板中 OPTIONS 请求的响应状态码和 CORS 响应头,确认方法与头部是否在服务器的允许列表中。
预检缓存
Access-Control-Max-Age 让浏览器在指定时长内对同一 URL 不再发起预检。不同浏览器有各自的缓存上限:
| 浏览器 | 最大缓存时间 | 备注 |
|---|---|---|
| Chromium(Chrome/Edge) | 7200 秒(2 小时) | 硬编码值,超过该值会被截断为 7200 |
| Firefox | 86400 秒(24 小时) | — |
实际应用中,两小时内对同一 URL 的重复调用(如单页应用内的连续请求)除第一个请求外,预检开销基本可以忽略。
预检缓存以 (URL, Origin) 为粒度存储,https://api.example.com/v1/users 和 https://api.example.com/v1/posts 的缓存相互独立。
基本用法
Nginx 配置
nginx
add_header Access-Control-Max-Age 7200;Chromium 的上限是 7200 秒,设置的值超过该上限只会被截断,因此建议直接指定 7200 以获得最大缓存时间。如果 OPTIONS 请求的响应状态码不是 Nginx 默认添加头部的那些(200、201、204、206、301、302、303、304、307、308),则需要加上 always 参数:
nginx
add_header Access-Control-Max-Age 7200 always;这样即使预检返回 204 或其他状态码,Nginx 也会正确输出该响应头。
Node.js(cors 中间件)
javascript
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: 'https://myapp.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 7200 // 对应 Access-Control-Max-Age,单位秒,设为 Chromium 允许的最大值
}));
app.get('/api/data', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(3000);中间件收到 OPTIONS 预检请求时会自动返回对应的 CORS 响应头,同时设置 Access-Control-Max-Age: 7200,后续在缓存有效期内的同 URL 请求不会再触发预检。
注意点
减少预检的常见做法及其权衡
将请求保持在简单请求范围内。 如果 API 只有 GET 且不携带自定义头,不会产生 OPTIONS 往返。但实际接口中
application/json和Authorization头普遍存在,多数请求无法避免预检。合理设置 Access-Control-Max-Age。 按 Chromium 的 7200 秒上限设定缓存时长,可以减少短时间内对同一 URL 的重复预检。
通过代理将请求变为同源。 开发阶段可以使用 Vite 或 Webpack 的 proxy 功能转发 API 请求:
tsexport default defineConfig({ server: { proxy: { '/api': { target: 'https://api.example.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, }, }, });部署时可以用 Nginx 反向代理将
/api/请求转发到实际 API 服务:nginxlocation /api/ { proxy_pass https://api.example.com/; proxy_set_header Host api.example.com; }代理使得从浏览器角度看,所有请求的源与页面源一致,不存在跨域,从根本上消除了预检。
避免附带非必要的自定义头部。 如果不需要区分不同源且不涉及 Cookie,可以考虑将认证信息通过查询参数传递(如
?token=xxx),以替代Authorization头。但这种做法会使 token 暴露在 URL 中,可能被浏览器历史、服务器日志等记录。另一种方式是使用 Cookie 配合SameSite=None; Secure,同时服务端需要返回Access-Control-Allow-Credentials: true。利用 CDN 统一处理 CORS。 可以在 CDN 层面统一配置 CORS 响应头,避免在各后端服务中单独处理。注意很多 CDN 默认不缓存 OPTIONS 方法,需要确认 CDN 能正确转发 OPTIONS 请求。
预检本身只是一次轻量级的 HTTP 往返——无请求体、无响应体,其开销远小于 application/json 的数据表达能力或 Authorization 头的安全性带来的收益。因此,通常情况下不值得为了省去一次预检而牺牲安全性。
