跳到主要内容

CORS 跨域与 Options 预检请求:为什么有时会先发一个 OPTIONS?

面试速答(30 秒版 TL;DR)

  • 跨域本质上不是“请求发不出去”,而是 浏览器的同源策略限制前端 JS 读取跨源响应
  • CORS(Cross-Origin Resource Sharing)是一套浏览器与服务器协作的跨域授权机制,核心靠的是一组响应头。
  • 简单请求不会预检;非简单方法、非简单头、某些非简单 Content-Type 往往会触发 OPTIONS 预检。
  • 预检请求的目的不是拿业务数据,而是先问服务器:这个跨域请求你允不允许、允许哪些方法和头。
  • 面试高频坑:Access-Control-Allow-Origin: * 不能和 Access-Control-Allow-Credentials: true 同时使用。

一、先分清“跨域”到底限制了什么

很多人以为跨域是“浏览器不能发请求”,这不准确。
更准确的说法是:

  • 浏览器可以把请求发出去
  • 服务器也可能正常响应
  • 但如果不满足同源策略和 CORS 规则,浏览器会阻止前端脚本读取响应

所以跨域是 浏览器安全模型,不是 TCP/IP 层面的障碍。


二、什么叫同源

通常要同时满足这三项:

  • 协议相同
  • 域名相同
  • 端口相同

例如:

  • https://a.com:443/page
  • https://a.com/api

这两个通常算同源。
但如果变成:

  • http://a.com
  • https://a.com

协议不同,就跨源了。


三、CORS 是怎么工作的

最简化理解:

  1. 浏览器发跨域请求时,会自动带上 Origin
  2. 服务端用 Access-Control-* 响应头表态
  3. 浏览器根据这些头决定是否把响应交给 JS

四、什么是简单请求

简单请求通常满足这些条件:

  • 方法是 GETHEADPOST
  • 请求头是浏览器允许的简单头
  • Content-Type 属于简单类型之一,例如:
    • text/plain
    • application/x-www-form-urlencoded
    • multipart/form-data

这类请求一般不会先发预检。


五、什么情况下会触发预检请求

典型触发条件:

  • 使用了 PUTDELETEPATCH 等非简单方法
  • 自定义请求头,例如 AuthorizationX-Trace-Id
  • Content-Type: application/json
  • 某些需要凭证的复杂跨域场景

这时浏览器会先自动发一个 OPTIONS 请求。


六、OPTIONS 预检到底在问什么

预检请求一般会带这些头:

Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type

它本质上在问服务端三件事:

  1. 这个源能不能访问你
  2. 这个方法能不能用
  3. 这些请求头能不能带

服务端如果允许,会返回类似:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600

然后浏览器才会决定是否发送真正的业务请求。


七、最常见的 CORS 响应头

Access-Control-Allow-Origin

允许哪个源访问。
可以是具体源,例如:

Access-Control-Allow-Origin: https://app.example.com

Access-Control-Allow-Credentials

是否允许携带凭证,例如 Cookie、Authorization 头对应的认证场景。

Access-Control-Allow-Credentials: true

Access-Control-Allow-Methods

允许的跨域方法列表。

Access-Control-Allow-Headers

允许的非简单请求头列表。

Access-Control-Expose-Headers

默认情况下,前端 JS 能读取的响应头是有限的;如果你希望 JS 读取自定义响应头,需要显式暴露。

Access-Control-Max-Age

预检结果可缓存多久。
这个值越合适,重复预检带来的额外 RTT 越少。


八、为什么有时后端明明返回了 200,前端还是报跨域

因为问题不在业务处理成不成功,而在:

  • 响应头是否正确
  • 是否允许当前 Origin
  • 是否允许携带的头/方法
  • 是否在有凭证场景下错误使用了 *

所以经常会出现:

  • Network 面板看见接口成功返回
  • 控制台却报 CORS error

这是典型的浏览器拦截“响应暴露”。


九、Cookie、Token 与 CORS 怎么一起讲

如果前端想跨域携带 Cookie,需要同时满足:

  • 前端请求开启 credentials
  • 服务端返回 Access-Control-Allow-Credentials: true
  • Access-Control-Allow-Origin 不能是 *
  • Cookie 本身还要满足 SameSiteSecure 等策略

例如前端:

fetch("https://api.example.com/user", {
credentials: "include",
});

Token 场景

如果把 Token 放在 Authorization 头里,通常会触发预检,因为它属于非简单头。


高频题标准答法

1. 为什么会先发一个 OPTIONS

因为浏览器发现这是一个非简单跨域请求,需要先确认服务端是否允许这个源、方法和请求头,避免直接发业务请求带来安全风险。

2. 表单 POST 为什么有时不预检,JSON POST 却预检?

因为 application/x-www-form-urlencodedmultipart/form-data 属于简单请求允许的内容类型;
application/json 通常不属于简单请求,因此常触发预检。

3. 服务端设置 Access-Control-Allow-Origin: * 就万事大吉吗?

不是。
如果有凭证需求,这样配置反而不合法;另外它也无法精细控制来源。

4. CORS 能防接口被别人直接调用吗?

不能。
CORS 主要限制的是浏览器脚本读取响应,不是替代鉴权。服务端该做的认证授权一点都不能少。


易错点 / 坑

  • 把跨域理解成“请求不能发出去”。
  • 忽略 OPTIONS 响应本身也要带正确的 CORS 头。
  • Allow-Origin: *Allow-Credentials: true 乱配。
  • 以为 CORS 能替代身份认证。

速记要点(可背诵)

  • 跨域是 浏览器同源策略 问题。
  • CORS 核心是:浏览器带 Origin,服务端回 Access-Control-*
  • 复杂跨域请求会先发 OPTIONS 预检。
  • Authorizationapplication/jsonPUT/DELETE 常是预检触发点。