预检请求触发条件为:使用PUT/DELETE等非简单方法、携带Authorization等自定义头、或Content-Type非text/plain等三类之一;其响应必须独立拦截、早于业务逻辑、避免头覆盖,且带凭证时Origin不可为*。

跨域请求出问题,八成卡在 OPTIONS 预检这一步——不是后端没配 CORS,而是预检响应没走对、头被覆盖、或逻辑被绕过。关键不在“加不加头”,而在“什么时候加、加给谁、会不会被后续代码冲掉”。
预检请求触发的准确条件
浏览器不会对所有跨域请求都发 OPTIONS。只有满足任一条件时,才会自动发起预检:
- 使用非简单方法:PUT、DELETE、PATCH、CONNECT、TRACE
- 携带自定义请求头:如 Authorization、X-Request-ID、X-Auth-Token
- Content-Type 不是以下三者之一:application/x-www-form-urlencoded、multipart/form-data、text/plain
例如:前端用 fetch('/api/user', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer xxx' } }) —— 这个请求一定会触发 OPTIONS 预检,因为同时含非简单方法 + 自定义头 + 非简单 Content-Type。
OPTIONS 响应必须独立且早于业务逻辑
很多失败源于把预检处理混在主路由里,比如在 PHP 或 Node.js 中先做鉴权、查数据库、写日志,最后才判断是不是 OPTIONS。这时即使返回了 200,也可能因前面已输出内容导致 header 无法设置,或者状态码被覆盖为 500/401。
- PHP:必须在任何
echo、print、HTML 输出前,用header()设置 CORS 头,并立即exit()或die() - Go:应在 handler 最开头用
if r.Method == "OPTIONS"拦截,调用w.WriteHeader(204)后直接 return - Nginx:用
if ($request_method = 'OPTIONS')块统一响应,避免转发到后端
Header 覆盖与顺序陷阱
CORS 头不是“加一次就完事”。多个中间件、反向代理、框架默认行为可能互相覆盖:
- Access-Control-Allow-Origin 只能出现一次;若 Nginx 和后端都设了,后者会被忽略(Nginx 在最外层)
- Spring Boot 的
@CrossOrigin注解和全局WebMvcConfigurer冲突时,注解优先级更高,但若路径不匹配则失效 - FastAPI 的
CORSMiddleware默认会拦截 OPTIONS,但如果手动在路由中又写了if request.method == "OPTIONS",反而可能跳过中间件逻辑,造成头缺失 - 带凭证(
credentials: true)时,Access-Control-Allow-Origin 不能为 *,必须精确指定域名,否则浏览器直接拒绝
生产环境必须校验 Origin,而非放行所有
开发时用 * 看似省事,但上线后会引发两个严重问题:
- 无法携带 Cookie 或 Authorization 头(浏览器禁止
Allow-Origin: *+Allow-Credentials: true共存) - 暴露接口给任意站点,增加 CSRF 和数据爬取风险
正确做法是白名单校验:
- 后端读取请求头中的 Origin 字段
- 比对是否在可信列表(如
['https://app.example.com', 'https://admin.example.com']) - 匹配成功则原样回传该 Origin 值,不匹配则不返回
Access-Control-Allow-Origin(浏览器视为拒绝)











