CORS 预检是什么
同源策略默认禁止页面跨域读取响应。当跨域请求「不够简单」时,浏览器会先发一条 OPTIONS 预检,问服务器「我来自 X,想用 PUT,并带这些头,允许吗」;只有收到明确的许可响应,浏览器才发真实请求。因此预检是浏览器主动发起的安全问询,而不是业务接口的一部分。
基本信息
| 触发条件 | 方法非 GET/HEAD/POST,或 Content-Type 非三类简单类型,或带自定义头 |
|---|---|
| 预检报文 | OPTIONS + Origin + Access-Control-Request-Method + Access-Control-Request-Headers |
| 通过条件 | 返回 2xx 且带匹配的 Access-Control-Allow-* |
| 缓存时长 | 由 Access-Control-Max-Age 控制 |
| 规范 | WHATWG Fetch 标准(CORS) |
三个关键响应头
| 响应头 | 作用 | 常见坑 |
|---|---|---|
| Access-Control-Allow-Origin | 声明允许的来源 | 带凭证时不能写 * |
| Access-Control-Allow-Methods | 列出允许的方法 | 漏写 PATCH 会让部分接口整体不可用 |
| Access-Control-Allow-Headers | 列出允许的请求头 | 漏写自定义头(如 x-token)预检直接失败 |
一次完整预检
一次完整预检
OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, x-token
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, PATCH
Access-Control-Allow-Headers: content-type, x-token
Access-Control-Max-Age: 600
Vary: Origin典型使用场景
- 前后端分离部署:前端域名与 API 域名不同,所有写操作都要预检
- 带自定义鉴权头(Authorization 或 x-token)的接口
- 提交 JSON(Content-Type: application/json)的写接口
- 使用 PUT/PATCH/DELETE 的 REST 接口
- 需要携带 Cookie 的跨域请求,还要额外开 Allow-Credentials
排查顺序
- 先读浏览器控制台的具体报错,它通常直接指明缺哪个响应头
- 用 curl 手工发 OPTIONS 打后端,绕过浏览器确认服务端的真实响应
- 检查是否被网关或鉴权中间件拦成 401/405,预检失败最常出在这一层
- 确认反向代理把 OPTIONS 转发给了后端,而不是自己吞掉或重定向
- 带凭证时确认 ACAO 是具体来源,且 Allow-Credentials 为 true
预检失败不等于接口故障:服务端日志里往往连真实请求都看不到,先解决 OPTIONS 的响应,再谈业务逻辑。
减少预检的手段
- 能不用自定义头就不用,改由请求体字段携带信息,可退回简单请求
- Content-Type 尽量使用简单类型,代价是请求体的表达力下降
- 把 Access-Control-Max-Age 设到浏览器允许的上限附近,减少重复预检
- 同域部署或用同域路径反向代理,从架构上绕开跨域问题