跨域资源共享 (CORS) 与同源策略

面试回答: 同源策略是浏览器限制跨源脚本读取敏感数据的安全边界,同源要求协议、主机和端口一致。CORS 是服务器通过 HTTP 响应头声明跨源读取权限、浏览器负责执行校验的协议。满足安全列表条件的请求可以直接发送,否则浏览器会先发 OPTIONS 预检;无论是否预检,实际响应都必须通过 CORS 校验。CORS 只决定前端能否读取响应,不负责身份认证,也不能替代 CSRF 防护。

1. 同源策略 (Same-Origin Policy)

浏览器中的一个源(Origin)由 URL 的协议、主机和端口组成,三者完全一致才属于同源。

https://www.example.com:443 为基准:

URL是否同源原因
https://www.example.com/profile协议、主机、端口相同,路径不影响源
http://www.example.com协议不同
https://api.example.com主机不同
https://www.example.com:8443端口不同

同源策略不是“禁止一切跨源请求”,而是限制不同源之间的交互能力,尤其是读取数据

  • 跨源脚本不能随意读取另一个页面的 DOM;
  • localStorage、IndexedDB 等存储按源隔离;
  • fetch / XHR 可以发出部分跨源请求,但脚本读取响应需要通过 CORS;
  • 表单提交、链接跳转以及 <img><script> 等资源嵌入在一定条件下可以跨源。

这个边界可以避免恶意页面直接读取用户在网银、邮箱等其他网站中的敏感页面和数据。

同源不等于同站。 Cookie 的 Domain、Path、Secure、SameSite 等规则与 Origin 判断不是同一套模型。例如两个子域可能属于同站,但仍然不同源。分析 Cookie、CORS 和 CSRF 时必须分别判断。

2. CORS (Cross-Origin Resource Sharing)

CORS 是建立在 HTTP 之上的一套协议:服务器通过响应头声明“这个响应可以共享给哪些源”,浏览器负责校验并决定是否把响应内容交给前端 JavaScript。

因此,CORS 主要限制的是跨源响应能否被前端读取,不等于请求一定没有发送到服务器。使用 curl、Postman 或服务端代码发请求时,也不会受到浏览器 CORS 校验的限制。

一个典型的跨源 fetch / XHR 请求流程是:

  1. 浏览器判断目标 URL 是否跨源。
  2. 判断请求是否满足 CORS 安全列表;满足则直接发送,否则先发预检请求。
  3. 浏览器在请求中添加 Origin,服务器返回相应的 Access-Control-Allow-* 响应头。
  4. 浏览器校验响应头;校验失败时,前端只能得到类似网络错误的结果,不能读取响应内容。

CORS 请求通常分为:简单请求(更准确地说是满足 CORS 安全列表的请求)需要预检的请求

(1) 简单请求

简单请求不会先发 OPTIONS,但必须同时满足以下核心条件:

  1. 请求方法只能是 GETHEADPOST
  2. 只能使用 CORS 安全列表中的请求头,例如 AcceptAccept-LanguageContent-LanguageContent-Type 和满足限制的 Range
  3. 如果设置了 Content-Type,其媒体类型只能是:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  4. 不能主动携带 AuthorizationX-Token 等非安全列表请求头。

规范对安全列表请求头的值、长度和字符还有更细的限制。面试中最常见的判断点是:请求方法、Content-Type 和自定义请求头。

例如,下面的跨源请求可以直接发送:

POST /api/profile HTTP/1.1
Host: api.example.com
Origin: https://www.example.com
Content-Type: text/plain

服务器允许该源读取响应时,可以返回:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Vary: Origin
Content-Type: application/json

如果服务器没有返回匹配的 Access-Control-Allow-Origin,这个 POST 请求通常已经到达服务器并可能被执行,只是浏览器不会把响应开放给前端读取。这也是为什么 CORS 不能替代 CSRF 防护、身份认证和权限校验

Vary: Origin 适合服务器根据请求的 Origin 动态返回不同 CORS 响应头的场景,可以避免共享缓存错误地复用其他源的响应。

(2) 预检请求(Preflight)

只要不满足简单请求条件,浏览器就会在实际请求前自动发送 OPTIONS 预检。例如:

  • 使用 PUTPATCHDELETE 等非安全列表方法;
  • 发送 Content-Type: application/json
  • 添加 AuthorizationX-Token 等非安全列表请求头。

因此,POST 并不一定是简单请求,常见的 POST + application/json 就会触发预检。

假设前端准备发送:

PUT /api/user/1 HTTP/1.1
Origin: https://www.example.com
Content-Type: application/json
X-Token: abc

浏览器会先自动发送:

OPTIONS /api/user/1 HTTP/1.1
Host: api.example.com
Origin: https://www.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://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, X-Token
Access-Control-Max-Age: 86400
Vary: Origin

浏览器确认目标源、方法和请求头都被允许后,才会继续发送实际的 PUT 请求。需要特别注意:

  • OPTIONS 预检通过,不代表实际响应自动通过;实际响应仍然要返回匹配的 Access-Control-Allow-Origin
  • 如果实际请求携带凭证,预检响应和实际响应还需要正确配置 Access-Control-Allow-Credentials: true
  • 预检请求本身不会携带跨源 Cookie 等凭证,服务端不应只依赖登录 Cookie 来放行 OPTIONS
  • Access-Control-Max-Age 缓存的是预检结果,它使用浏览器内部独立的预检缓存;浏览器可以限制最大缓存时间或提前清除缓存。
  • 预检由浏览器自动完成,前端不能通过手动添加 CORS 响应头来跳过它。

(3) 常用 CORS 请求头与响应头

请求头 / 响应头方向作用
Origin浏览器请求表示请求来自哪个源,由浏览器设置
Access-Control-Request-Method预检请求告知服务器实际请求准备使用的方法
Access-Control-Request-Headers预检请求告知服务器实际请求准备携带的非安全列表请求头
Access-Control-Allow-Origin服务器响应允许哪个源读取响应;凭证请求不能使用 *
Access-Control-Allow-Methods预检响应允许实际请求使用哪些方法
Access-Control-Allow-Headers预检响应允许实际请求携带哪些请求头
Access-Control-Allow-Credentials服务器响应是否允许浏览器向前端暴露带凭证请求的响应
Access-Control-Expose-Headers实际响应允许前端读取哪些非安全列表响应头
Access-Control-Max-Age预检响应指定预检结果可以缓存多少秒

即使 CORS 请求成功,前端默认也只能读取 Cache-ControlContent-LanguageContent-LengthContent-TypeExpiresLast-ModifiedPragma 等安全列表响应头。读取自定义响应头时,需要服务端显式暴露:

Access-Control-Expose-Headers: X-Request-Id, X-RateLimit-Remaining

(4) 高频误区

  • mode: "no-cors" 不能解决跨域。 它会限制可用的方法和请求头,并让前端得到不可读取内容和响应头的 opaque response。
  • CORS 不是服务端防盗链或接口鉴权。 非浏览器客户端可以直接请求接口,服务端仍需进行身份认证和权限校验。
  • 不要无条件回显任意 Origin 需要携带凭证时,应维护可信源白名单,并返回匹配的具体源。
  • 请求成功不代表 CORS 成功。 Network 面板可能看到服务器返回 200,但只要 CORS 响应头不匹配,前端仍然无法读取响应。

规范依据:Fetch Standard:CORS protocol

fetch 为例,跨源请求默认不会因为存在登录 Cookie 就自动携带凭证。需要同时满足以下条件:

  1. 前端设置 credentials: 'include';XHR 则设置 withCredentials = true
  2. Cookie 自身的 Domain、Path、Secure 和 SameSite 规则允许发送。
  3. 服务端返回匹配当前请求源的 Access-Control-Allow-Origin,不能使用 *
  4. 服务端返回 Access-Control-Allow-Credentials: true
fetch('https://api.example.com/profile', {
  credentials: 'include'
});
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

浏览器是否发送 Cookie 与前端是否能读取响应是两层判断。即使 Cookie 已发送,响应缺少正确的 CORS 头时,前端仍然读不到响应内容。

4. JSONP (旧方案)

JSONP 利用 <script> 标签不受同源策略限制。

  • 原理:动态插入 <script src="https://api.example.com?callback=fn">,服务端返回可执行代码 fn({ data: ... })
  • 限制:只支持 GET,缺少标准的错误处理,并且等同于执行对方返回的脚本,存在明显安全风险。

现代项目应优先使用 CORS,JSONP 主要用于理解历史跨域方案。

5. 其他解决方案

  • 开发服务器或 Nginx 反向代理:浏览器请求同源地址,由服务器转发到真正的后端。服务端之间的请求不执行浏览器 CORS 校验。
  • postMessage:用于窗口、标签页和 iframe 之间的跨源消息通信。接收方必须校验 event.origin,发送方不要随意使用 * 作为目标源。
  • WebSocket:握手不使用 CORS 协议,但浏览器会发送 Origin,服务端仍应校验允许的来源并完成鉴权。

这些方案解决的问题不同:代理用于 HTTP 接口转发,postMessage 用于窗口通信,WebSocket 用于双向长连接,不能互相当作通用替代品。

6. 面试总结

  • 同源比较的是协议、主机和端口;同源与 Cookie 的同站规则不同。
  • CORS 由服务端声明授权、浏览器执行校验,非浏览器客户端不受这套读取限制。
  • 简单请求直接发送;不满足安全列表条件的请求先预检,再发送实际请求。
  • CORS 失败不代表请求一定没到服务器,所以它不能替代鉴权、权限校验和 CSRF 防护。
  • 凭证请求需要前后端和 Cookie 属性共同满足条件,且 Access-Control-Allow-Origin 不能为 *