CORS 预检请求详解:何时触发 OPTIONS 请求?
大家好,欢迎来到今天的讲座!我是你们的技术讲师,今天我们要深入探讨一个在现代 Web 开发中非常关键但常常被误解的话题——CORS(跨源资源共享)中的预检请求(Preflight Request)。你可能已经遇到过这样的场景:前端发起一个 POST 请求到另一个域名的 API,浏览器却先发送了一个 OPTIONS 请求,然后才真正执行你的请求。这背后到底发生了什么?为什么浏览器要这么做?
我们不会讲“官方文档式的理论”,而是从真实开发者的视角出发,结合代码、逻辑和常见陷阱,带你彻底理解这个机制。
一、什么是 CORS?为什么需要它?
在 Web 安全体系中,浏览器实施了同源策略(Same-Origin Policy),即只有当请求的协议、域名、端口完全一致时,脚本才能访问响应内容。这是为了防止恶意网站通过 JavaScript 获取其他站点的数据。
但是,在实际开发中,我们经常需要让前端(比如部署在 http://localhost:3000)调用后端 API(比如部署在 https://api.example.com)。这就产生了“跨域”问题。
为了解决这个问题,W3C 标准引入了 CORS(Cross-Origin Resource Sharing) 协议。它允许服务器明确告诉浏览器:“我可以接受来自某些来源的请求”,从而绕过同源限制。
然而,CORS 并不是简单地加个头就能搞定的。它有一个“预检机制”——这就是我们今天的核心话题:OPTIONS 请求。
二、什么时候会触发 OPTIONS 请求?
✅ 触发条件总结表:
| 条件 | 是否触发 OPTIONS |
|---|---|
| 同源请求(协议+域名+端口相同) | ❌ 不触发 |
| 异源请求 + 使用简单请求方法(GET/HEAD/POST)且无自定义头部 | ❌ 不触发 |
| 异源请求 + 使用非简单请求方法(PUT, DELETE 等) | ✅ 触发 |
| 异源请求 + 使用自定义 Header(如 Authorization) | ✅ 触发 |
| 异源请求 + Content-Type 不是 application/x-www-form-urlencoded / multipart/form-data / text/plain | ✅ 触发 |
🔍 注意:这里的“简单请求”是指符合以下两个条件的请求:
- 方法只能是 GET、HEAD 或 POST;
- 请求头只能包含 Accept、Accept-Language、Content-Language、Last-Event-ID 和 Content-Type(仅限 application/x-www-form-urlencoded、multipart/form-data、text/plain)。
一旦不满足这两个条件,浏览器就会自动发出一个 OPTIONS 预检请求,询问服务器是否允许这次跨域请求。
三、举个例子:触发 OPTIONS 的典型场景
假设你在前端写了一个这样的 Axios 请求:
axios.post('https://api.example.com/users', {
name: 'Alice',
email: '[email protected]'
}, {
headers: {
'Authorization': 'Bearer abc123',
'X-Custom-Header': 'my-value'
}
});
这个请求会触发 OPTIONS 请求!因为:
- 它是跨域请求;
- 使用了
Authorization自定义 header; - 虽然是 POST,但因为有自定义 header,所以不再是“简单请求”。
浏览器行为流程如下:
-
第一步:发送 OPTIONS 请求
OPTIONS /users HTTP/1.1 Host: api.example.com Origin: http://localhost:3000 Access-Control-Request-Method: POST Access-Control-Request-Headers: authorization,x-custom-header -
第二步:服务器响应 OPTIONS
如果服务器配置正确,返回:HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: authorization, x-custom-header Access-Control-Max-Age: 86400 -
第三步:浏览器再发送真正的 POST 请求
POST /users HTTP/1.1 Host: api.example.com Origin: http://localhost:3000 Authorization: Bearer abc123 X-Custom-Header: my-value
如果服务器没有正确处理 OPTIONS 请求,或者返回了错误的状态码(如 403),浏览器将直接终止整个请求,并报错:
Access to XMLHttpRequest at 'https://api.example.com/users' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
四、如何避免不必要的 OPTIONS 请求?
方案 1:使用简单请求(推荐)
如果你只需要发送基本数据,比如登录表单提交,可以这样改:
// 前端代码(保持简单)
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'username=admin&password=secret'
});
此时浏览器不会触发 OPTIONS 请求!
方案 2:服务端缓存 OPTIONS 响应(提高性能)
设置 Access-Control-Max-Age 可以让浏览器缓存预检结果一段时间(单位秒):
Access-Control-Max-Age: 86400 # 缓存一天
这意味着在这段时间内,即使再次发起相同类型的跨域请求,也不用再发 OPTIONS。
⚠️ 注意:一旦请求方式或 header 改变,缓存失效,必须重新预检。
五、Node.js Express 示例:正确处理 OPTIONS 请求
下面是一个完整的 Express 后端示例,展示如何优雅地支持 CORS 预检请求:
const express = require('express');
const app = express();
// 中间件:允许所有来源(生产环境建议具体化)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 处理 OPTIONS 请求
if (req.method === 'OPTIONS') {
res.status(200).send();
return;
}
next();
});
// 示例接口
app.post('/users', (req, res) => {
console.log('收到用户创建请求:', req.body);
res.json({ message: '用户已创建' });
});
app.listen(3001, () => {
console.log('服务器运行在 http://localhost:3001');
});
📌 关键点:
- 检查
req.method === 'OPTIONS'是必须的; - 返回空响应体 + 200 状态码即可;
- 不要忘记设置允许的方法和头字段。
否则,浏览器认为服务器拒绝了跨域请求,导致失败。
六、常见误区与踩坑指南
| 误区 | 正确做法 |
|---|---|
| “我在服务器上加了 CORS 头就万事大吉了” | ❌ 必须处理 OPTIONS 请求,否则无效 |
| “我用了 axios 就不需要关心 OPTIONS” | ❌ axios 不会帮你做预检,浏览器决定何时发 OPTIONS |
| “只要返回 200 就行了” | ❌ 必须包含正确的 Access-Control-* 头部,否则浏览器仍会拦截 |
| “我不懂为什么我的请求总是被阻止” | ✅ 打开浏览器开发者工具 → Network → 查看 OPTIONS 请求状态和响应头 |
🐞 错误案例:未处理 OPTIONS 导致的问题
// 错误写法:只处理了 POST,忽略了 OPTIONS
app.post('/users', (req, res) => {
res.json({ success: true });
});
// 结果:浏览器看到 OPTIONS 返回 405 Method Not Allowed
✅ 正确做法:
app.use((req, res, next) => {
if (req.method === 'OPTIONS') {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.status(200).send(); // 必须显式返回
return;
}
next();
});
七、高级技巧:使用 cors 中间件简化开发
为了避免手动编写复杂的 CORS 逻辑,推荐使用官方库 cors:
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// 全局启用 CORS(生产环境建议配置白名单)
app.use(cors());
// 或者更细粒度控制
app.use(cors({
origin: ['http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.post('/users', (req, res) => {
res.json({ message: '成功!' });
});
app.listen(3001);
这个中间件会自动处理 OPTIONS 请求,无需你自己写逻辑。非常适合快速开发阶段使用。
八、总结:记住三个要点
- 触发条件清晰:只要不是“简单请求”,浏览器就会发 OPTIONS。
- 服务端必须响应 OPTIONS:否则跨域请求永远失败。
- 合理利用缓存:设置
Access-Control-Max-Age提升性能。
九、课后思考题(可选练习)
-
如果你有一个 React 应用部署在
http://localhost:3000,调用一个 Flask 后端 API(http://localhost:5000),并且请求头包含X-API-Key,会发生什么?- ✅ 答案:会触发 OPTIONS 请求,除非后端明确允许该 header。
-
如何在 Nginx 中配置 CORS 支持?
-
✅ 答案:
location / { add_header Access-Control-Allow-Origin "*"; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type, Authorization"; if ($request_method = 'OPTIONS') { add_header Access-Control-Max-Age 86400; add_header Content-Length 0; add_header Content-Type text/plain; return 204; } }
-
希望这篇讲解能帮你彻底搞懂 OPTIONS 预检请求的本质。记住:这不是浏览器的“刁难”,而是保护你应用安全的重要机制。掌握它,你就离成为一名成熟的 Web 开发者更近一步!
如果你还有疑问,欢迎留言讨论 👇
祝你编码愉快!