跳到主要内容
浏览器
导航
网页浏览器

同源策略与 CORS 的机制解析

同源策略是浏览器隔离不同站点数据的基础机制。本文拆解源的判定规则、跨源访问受限的具体范围,以及 CORS 中简单请求与预检请求的完整流程,并说明各响应头的语义与凭证请求的判定条件。

发布于 4 分钟阅读 1315 字

浏览器会同时加载并执行来自多个站点的脚本。如果没有隔离,一段脚本就能读取另一个站点的响应内容、访问其页面中的节点,并把数据发往任意位置。同源策略(Same-Origin Policy)正是浏览器为切断这条路径而施加的隔离规则:它不阻止请求发出,而是限制一个源的文档能否读取另一个源的资源。

要理解这套规则,需要依次回答三个问题:源如何计算、哪些能力受到限制、CORS 又是如何在此基础上放宽限制的。

源的判定

源由协议、主机、端口三部分构成,任意一项不同,两个源就互不相同。

页面地址目标地址是否同源判定依据
https://a.example.com/xhttps://a.example.com/y同源三项完全一致
https://a.example.comhttp://a.example.com跨源协议不同
https://a.example.comhttps://b.example.com跨源主机不同
https://a.example.comhttps://a.example.com:8443跨源端口不同
https://a.example.comhttps://a.example.com:443同源省略端口时按协议默认值补齐

主机名比较时不区分大小写;端口省略时按协议默认端口补齐,http 为 80、https 为 443。同主域下的子域之间互不相同,a.example.com 与 b.example.com 是两个独立的源。早期 document.domain 可以把脚本的源降级到可注册域,从而让同主域的多个子域互通,该做法在较新的实现中已被弱化或移除。

受限的访问范围

同源策略限制的是「读取」,而不是「发送」。逐项来看:

  • DOM:跨源 iframe 的 contentWindow 可以拿到引用,但访问其文档内部的节点会被拒绝;postMessage 提供了受控的通信通道。
  • 网络响应:fetch 与 XMLHttpRequest 默认不能读取跨源响应体,请求本身仍会发出。
  • Cookie:Cookie 的可见性由 Domain、Path、Secure、SameSite 等属性决定,与源并非一一对应,属于另一套独立规则。
  • 存储:localStorage、sessionStorage、IndexedDB 按源隔离,脚本读不到其他源写入的数据。
  • 画布像素:把跨源图像绘制到 canvas 后,调用 getImageData 会因画布被标记为污染而失败。

需要注意的是,跨源请求可能带有副作用:表单提交与简单请求依然会把数据发到服务器,服务端难以从请求本身判断它源自哪个页面。这类问题通常由 CSRF 令牌与 SameSite 属性处理。

CORS:简单请求与预检

跨源资源共享(CORS)是一套基于响应头的授权机制:服务端显式声明允许哪些源访问,浏览器据此决定是否把响应交给脚本。

满足下列条件的请求按「简单请求」处理,直接发出,不产生额外往返:

  • 方法是 GET、HEAD 或 POST
  • 请求头只包含 CORS 安全列表中的字段,如 Accept、Accept-Language、Content-Language
  • Content-Type 只取 application/x-www-form-urlencoded、multipart/form-data、text/plain 三者之一

不满足上述条件的请求,浏览器会先发出一个 OPTIONS 预检请求,询问服务端是否允许实际请求:

OPTIONS /api/data HTTP/1.1
Origin: https://a.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://a.example.com
Access-Control-Allow-Methods: GET, PUT
Access-Control-Allow-Headers: content-type, x-token
Access-Control-Max-Age: 600

预检通过后,浏览器才发送实际请求,并在收到响应时再次校验 Access-Control-Allow-Origin。Access-Control-Max-Age 给出预检结果可被缓存的时间,用来减少重复的 OPTIONS 往返。

响应头的语义

响应头作用说明
Access-Control-Allow-Origin声明允许的源可取具体源或 *,携带凭证时不能为 *
Access-Control-Allow-Methods预检允许的方法只在预检响应中校验
Access-Control-Allow-Headers预检允许的请求头用于非安全列表字段
Access-Control-Expose-Headers暴露给脚本的响应头未列出的响应头脚本读不到
Access-Control-Allow-Credentials是否接受凭证为 true 时允许携带 Cookie
Access-Control-Max-Age预检缓存时长由浏览器维护,各实现的上限不同

携带凭证的请求

当请求以 credentials: 'include' 发出时,浏览器会附带 Cookie 与 HTTP 认证信息。此时服务端必须返回具体的源而非常量 *,并同时返回 Access-Control-Allow-Credentials: true,否则浏览器会拒绝把响应交给脚本。带凭证的跨源请求还受 Cookie 自身规则约束:若 Cookie 的 SameSite 取 Lax 或 Strict,默认不会随跨源请求发出。

JSONP 的历史成因与局限

在 CORS 尚未被广泛支持之前,页面常用 JSONP 绕过限制:<script> 的加载不受同源策略约束,因此把请求结果包装成对回调函数的调用,即可取回数据。

// 页面预先定义回调
function handle(payload) { console.log(payload); }
// <script src="https://api.example.com/data?cb=handle"></script>

JSONP 的局限来自它的本质:它执行的是第三方脚本,等价于把该域的代码引入页面,出错时也拿不到状态码;它只能发 GET 请求;回调名与参数需要双方约定。随着 CORS 的普及,多数场景已由 CORS 承担,JSONP 更多作为历史方案存在。

小结

同源策略以「协议 + 主机 + 端口」为隔离单位,限制跨源读取而非请求发送。CORS 通过一组响应头把授权决策交回服务端:简单请求直接发出并在响应上校验,非简单请求先经 OPTIONS 预检。理解这条链路,有助于排查跨源报错与凭证不生效等问题,也能与 Service Worker 拦截请求的机制对照来看。

网页浏览器 · 相关文章