跨域资源请求:简单请求与非简单请求
说到跨域,大家最先想到的估计就是那个烦人的 Access-Control-Allow-Origin。但你有没有发现,有时候浏览器报跨域错,有时候却悄无声息?
这里头其实分了两类——简单请求和非简单请求。
我当年刚接网关项目时,被这俩概念坑过好几次。
有一次前端死活调不通接口,我查了半天响应头都对,后来才发现是 Preflight 请求没通过。说白了,浏览器帮你做了个过滤器,但这个过滤器分两种模式。
什么是简单请求?
简单请求,指的是浏览器可以“直接”发送的跨域请求。
它不需要提前发个 OPTIONS 探路,一锤子买卖。
满足以下所有条件才算简单请求:
- 请求方法只能是 GET、HEAD、POST
- 请求头中只能出现浏览器允许的“安全首部集合”
- Content-Type 只能取:text/plain、multipart/form-data、application/x-www-form-urlencoded
- 不能使用 ReadableStream 或任何事件监听器(常见于 fetch)
用你们最常见的场景举个例子:
// 简单请求:GET + 默认头
fetch('https://api.example.com/user', {
method: 'GET',
headers: {
'Content-Type': 'text/plain' // 安全
}
})
这种情况下,浏览器直接发请求,然后看响应里有没有 Access-Control-Allow-Origin。
有且匹配,数据拿到;没有或匹配失败,前端 CORS 错误。
避坑提示:Content-Type 写成
我曾经有个项目,后端说“我支持跨域”,但前端一直报错,最后发现就是 Content-Type 没改回 text/plain。所以网关在设计时,通常会要求前端统一用 JSON,那就老老实实处理 Preflight 吧。
application/json 就直接变成非简单请求。我曾经有个项目,后端说“我支持跨域”,但前端一直报错,最后发现就是 Content-Type 没改回 text/plain。所以网关在设计时,通常会要求前端统一用 JSON,那就老老实实处理 Preflight 吧。
非简单请求:没那么简单
非简单请求,浏览器认为自己拿不准,所以先派一个“探子”——也就是 Preflight 预检请求。
这个探子用 OPTIONS 方法发,问服务器:“我能发这个请求吗?”
服务器回一个“可以”,浏览器才继续发正式请求。
什么情况下会触发 Preflight?
- 使用了非简单方法:PUT、DELETE、PATCH、CONNECT 等
- 自定义了非安全首部:比如
Authorization、X-Requested-With等 - Content-Type 不是那三个简单值
- 请求使用了 ReadableStream 或 Blob 等复杂体
看一个典型例子:
// 非简单请求:POST + application/json + 自定义头
fetch('https://api.example.com/order', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Auth': 'token123'
},
body: JSON.stringify({ product: 'book' })
})
浏览器先发 OPTIONS:
OPTIONS /order HTTP/1.1
Origin: https://frontend.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: x-custom-auth, content-type
服务器必须这样回应:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Custom-Auth
Access-Control-Max-Age: 86400
收到允许后,浏览器才发真正的 POST 请求。
注意:很多后端新手只给响应加
结果就是 Preflight 直接 404 或 500,前端永远拿不到数据。
我有个线上事故就是这样发生的——网关层没有配置 OPTIONS 路由,整个前端瘫痪了半小时。所以,网关必须显式处理 OPTIONS 请求并返回必要首部。
Access-Control-Allow-Origin,忘了处理 OPTIONS 请求。结果就是 Preflight 直接 404 或 500,前端永远拿不到数据。
我有个线上事故就是这样发生的——网关层没有配置 OPTIONS 路由,整个前端瘫痪了半小时。所以,网关必须显式处理 OPTIONS 请求并返回必要首部。
两种请求对比
| 对比维度 | 简单请求 | 非简单请求 |
|---|---|---|
| 请求链 | 直接发请求 + 响应 | OPTIONS(预检)+ 正式请求 + 响应 |
| 浏览器行为 | 实时检查响应头 | 先检查预检响应,再发实际请求 |
| 典型方法 | GET, POST, HEAD | PUT, DELETE, PATCH 等 |
| Content-Type | text/plain, multipart/form-data, x-www-form-urlencoded | application/json 及其他 |
| 自定义头 | 不允许 | 允许,但必须显式声明 |
| 性能影响 | 0 额外开销 | 多一次 OPTIONS 往返(可缓存) |
Preflight 的缓存机制
你可能会想:每次请求都多一次 OPTIONS,岂不是慢死了?
其实浏览器允许用 Access-Control-Max-Age 控制预检的缓存时间,单位秒。
- 设置
Access-Control-Max-Age: 86400可以缓存一天 - 缓存期间,同域同方法同头的请求不会重复发 Preflight
- 但注意,浏览器上限通常为 24 小时(各浏览器有差异)
我建议在网关层统一返回 7200(两小时) 作为默认值。既避免频繁探路,又不会让缓存过期后产生瞬间的延迟波动。
完整流程对比图
网关层面的处理策略
了解这些后,我们再看网关怎么设计。
- 统一拦截 OPTIONS:网关层对所有跨域请求预检直接返回 204,并带上白名单的 Origin、Methods、Headers。
- 动态允许 Origin:不要写死
*,尤其是带凭据的请求。我一般会让网关从配置文件读取允许域名列表,运行时校验。 - 合理设置 Max-Age:建议 7200 秒,配合后端配置。
- 注意浏览器兼容性:Safari 对 Preflight 缓存时间有限制,我曾遇到 iOS 设备上 Preflight 频繁发送导致的延迟抖动,最后把 Max-Age 降到了 600 秒才稳定。
核心原则: 网关是 CORS 的第一道关卡。
不要把跨域逻辑散落到各个微服务里,统一在网关层处理,否则你会陷入每个服务都要加一堆头的噩梦。
我个人习惯是:网关层全局配置 CORS 规则,后端微服务只管自己的业务逻辑。
不要把跨域逻辑散落到各个微服务里,统一在网关层处理,否则你会陷入每个服务都要加一堆头的噩梦。
我个人习惯是:网关层全局配置 CORS 规则,后端微服务只管自己的业务逻辑。
好了,简单请求和非简单请求的区别就这些。
记住一句话:凡是浏览器拿不准的,都会先探个路。而网关要做的,就是当好这个“路标”。