1. 问题现象与背景解析
最近在调试一个前后端分离项目时,发现浏览器对同一个API接口发起了两次请求:第一次是OPTIONS方法,第二次才是真正的GET/POST请求。这种现象在跨域场景下尤为常见,但很多刚接触前端开发的同学会感到困惑——明明代码里只调用了一次接口,为什么开发者工具里会显示两次请求?
要理解这个现象,我们需要从浏览器的同源策略说起。同源策略(Same-Origin Policy)是浏览器最基本的安全机制之一,它限制了一个源的文档或脚本如何与另一个源的资源进行交互。当协议、域名、端口三者有任一不同时,就会触发跨域限制。而CORS(Cross-Origin Resource Sharing)正是为了解决跨域问题而设计的W3C标准。
关键提示:OPTIONS请求是浏览器自动发起的"预检"(Preflight)请求,开发者无法通过代码控制或取消这个行为,它是CORS机制的重要组成部分。
2. CORS预检机制深度剖析
2.1 什么情况下会触发预检请求
并非所有跨域请求都会触发OPTIONS预检。根据CORS规范,满足以下所有条件的"简单请求"不会触发预检:
- 使用GET、HEAD或POST方法
- 仅包含安全的头部字段(Accept、Accept-Language、Content-Language等)
- Content-Type仅限于text/plain、multipart/form-data或application/x-www-form-urlencoded
而当请求满足以下任一条件时,浏览器就会先发送OPTIONS请求:
- 使用了PUT、DELETE等非简单方法
- 包含自定义头部(如X-Auth-Token)
- Content-Type为application/json
- 请求中使用了Credentials
2.2 预检请求的工作流程
一个完整的预检流程如下:
- 浏览器检测到跨域请求不符合简单请求条件
- 自动发送OPTIONS请求到目标服务器,包含:
- Access-Control-Request-Method:声明实际请求将使用的方法
- Access-Control-Request-Headers:声明实际请求将携带的自定义头部
- 服务器响应必须包含:
- Access-Control-Allow-Origin:允许的源
- Access-Control-Allow-Methods:允许的方法
- Access-Control-Allow-Headers:允许的头部
- 浏览器验证通过后,才会发送真正的请求
// 预检请求示例 OPTIONS /api/user HTTP/1.1 Host: target.com Access-Control-Request-Method: DELETE Access-Control-Request-Headers: X-Custom-Header // 服务器成功响应 HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://source.com Access-Control-Allow-Methods: GET, POST, DELETE Access-Control-Allow-Headers: X-Custom-Header3. 实际开发中的解决方案
3.1 后端配置CORS的正确姿势
以Node.js + Express为例,完整的CORS中间件配置应该包含:
const corsOptions = { origin: ['https://your-domain.com', 'https://another-domain.com'], methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 86400 // 预检结果缓存24小时 } app.use(cors(corsOptions))关键配置项说明:
maxAge:控制预检结果的缓存时间,合理设置可减少OPTIONS请求次数credentials:当需要传递cookie等凭证信息时必须设为true- 生产环境务必指定具体的
origin,避免使用通配符*
3.2 前端优化策略
虽然无法避免OPTIONS请求,但可以通过以下方式优化:
- 合并请求:将多个小请求合并为一个
- 缓存预检结果:通过maxAge设置较长的缓存时间
- 避免不必要的自定义头部
- 对于简单请求场景,优先使用GET/POST方法
4. 常见问题排查指南
4.1 预检失败的典型表现
当遇到以下错误时,通常意味着CORS配置有问题:
Access-Control-Allow-Origin missingMethod not allowed in Access-Control-Allow-MethodsRequest header field xxx is not allowed
排查步骤:
- 检查OPTIONS请求是否返回200/204状态码
- 确认响应头包含必要的CORS字段
- 验证
Access-Control-Allow-Origin是否匹配请求源 - 检查
Access-Control-Allow-Methods是否包含实际请求方法
4.2 特殊场景处理
场景一:需要携带cookie的跨域请求
- 前端需要设置
withCredentials: true - 后端必须配置
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin不能使用通配符*
场景二:处理非标准HTTP方法某些框架可能使用非标准方法(如PATCH),需要在服务器显式声明:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE5. 进阶话题与性能优化
5.1 预检请求的性能影响
虽然单个OPTIONS请求开销不大,但在高频请求场景下可能成为性能瓶颈。实测数据表明:
- 未缓存的预检请求会增加100-300ms延迟
- 移动网络环境下延迟可能翻倍
优化建议:
- 设置合理的
maxAge(建议≥3600秒) - 对静态资源使用CDN,配置CORS头
- 考虑使用反向代理避免跨域
5.2 替代方案对比
除了CORS,跨域解决方案还有:
- JSONP:仅支持GET,安全性较差
- 代理服务器:增加架构复杂度
- postMessage:适合iframe通信
- WebSocket:不受同源策略限制
CORS仍然是目前最安全、标准的解决方案,适合大多数现代Web应用。
6. 实战经验分享
在实际项目中,我遇到过几个典型的CORS问题:
案例一:开发环境正常但生产环境跨域原因:开发环境使用webpack-dev-server代理,而生产环境Nginx未正确配置。解决方案是在Nginx中添加:
location /api { add_header 'Access-Control-Allow-Origin' '$http_origin'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; add_header 'Access-Control-Max-Age' 1728000; }案例二:第三方接口无法修改CORS头解决方案是搭建一个简单的Node.js代理层:
app.use('/proxy', async (req, res) => { const response = await axios.get('https://third-party.com/api', { params: req.query }) res.json(response.data) })案例三:移动端WebView特殊处理某些安卓WebView需要额外配置:
webView.getSettings().setAllowUniversalAccessFromFileURLs(true); webView.getSettings().setAllowFileAccessFromFileURLs(true);