同源策略
同源策略(Same-origin policy)是浏览器隔离不同源内容的一组规则,主要防止一个网站的脚本随意读取另一个网站的页面、响应和存储。它并不禁止一切跨域请求:图片可以展示,普通外部脚本可以执行,某些请求也已经到达服务器,但这些行为并不等于允许读取对方的所有数据。
面试中需要把三个问题分开:两个页面是否同源、当前操作属于加载还是读取、双方准备通过什么受控接口交换数据。CORS 的请求协商属于网络访问协议,本文先解释它需要放宽的边界。
1. 面试核心
先记住这几条,再读正文- 对普通 HTTP(S) 地址,协议、主机、端口都相同才同源;路径、查询参数、片段不参与判断。有效默认端口需要归一化。卡 · 普通地址的同源判断
- 同源策略保护浏览器中的跨源读取边界,防止一个网站借助用户的浏览器读取另一个网站的数据;它不能替代服务端鉴权或防止所有跨站写操作。卡 · 为什么要隔离读取
- 请求发出、资源加载、脚本读取是不同权限。跨源 GET 可能到达服务器但响应不可读,iframe 可以显示但 DOM 不可读。卡 · 同源策略具体限制什么
- 普通外部脚本在引入它的页面环境中执行,不是在脚本文件所属站点的隔离环境中执行;模块脚本的跨源加载还需要 CORS。卡 · 第三方脚本在哪里执行
- 跨源图片能显示,不代表画布像素可以读取。未经 CORS 授权的跨源图像会污染绘制它的画布。卡 · 图片显示与画布读取
- CORS 放行响应不会开放跨源 iframe 的 DOM。窗口通信使用
postMessage等明确接口,嵌入权限、脚本读取权限和沙箱权限分别判断。卡 · 嵌入不等于读取 postMessage接收方必须校验发送源、发送窗口和消息结构;发送方指定准确的targetOrigin,不能把收到的字符串直接当作 HTML 或代码。卡 · 使用 postMessage 交换限定的数据no-cors不会让任意跨源响应变得可读;代理、CORS、窗口消息和 JSONP 解决的问题也不相同。卡 · no-cors 响应为什么不能使用
2. 源与信任边界
2.1 普通地址的同源判断
源(origin)对普通 HTTP(S) URL 由协议、主机和端口共同确定。以 https://app.example.com/orders 为参照:
| 地址 | 是否同源 | 原因 |
|---|---|---|
https://app.example.com/profile?tab=info#name | 是 | 仅路径、查询和片段不同 |
https://app.example.com:443/orders | 是 | HTTPS 的有效默认端口是 443 |
http://app.example.com/orders | 否 | 协议不同 |
https://app.example.com:8443/orders | 否 | 端口不同 |
https://api.example.com/orders | 否 | 主机不同 |
https://example.com/orders | 否 | 主机不同,不因为父子域关系而同源 |
const base = new URL('https://app.example.com/orders');
console.log(base.origin);
console.log(new URL('https://app.example.com:443/profile').origin === base.origin);
console.log(new URL('https://api.example.com/orders').origin === base.origin);
// https://app.example.com
// true
// false
浏览器按 URL 规则规范化主机和默认端口。两个域名解析到同一个 IP,也不会因此同源;同一域名背后换了服务器,则不因此改变源。
同站(same-site)与同源不是一回事。例如同为 HTTPS 的 app.example.com 和 api.example.com 可以同站,但仍然跨源。Cookie 的 SameSite 属性、Domain 和 Path 不能直接套用同源判断,具体边界见 浏览器存储。
2.2 为什么要隔离读取
假设你已经登录了某个账户站点,同时打开另一个不可信网站。如果后者可以直接读取账户页的 DOM 或接口响应,就可能看到用户在前者的私有数据。服务端认为请求来自已登录浏览器,并不意味着浏览器里每个页面都有权读取结果。
同源策略让浏览器在客户端维护这条边界。但跨站请求是否携带 Cookie,还受 SameSite、凭据模式和隐私策略等条件影响;不能把“所有跨域请求都带登录态”当作前提。服务端仍要验证身份、权限,并保护有副作用的操作。
同源意味着页面通常拥有相近的脚本权限。因此,把彼此不可信的应用放在同一个源下的不同路径,不会自动形成隔离。代码运行在同源页面中,也不能指望同源策略限制它只读取某个页面区域。
2.3 不透明源与继承的源
不是所有文档的源都能直接从地址栏文本推导。没有额外沙箱限制的 about:blank、iframe srcdoc 等文档可以继承创建者或嵌入方的源。
不透明源(opaque origin)是不暴露普通协议、主机、端口组合的独立源。缺少 allow-same-origin 的沙箱 iframe 会使用这种源。它序列化为字符串时可能显示为 null,但两个不透明源都显示 null,并不意味着它们同源。文件 URL 的处理还存在浏览器差异,开发和验证页面应使用本地 HTTP 服务。
new URL(...).origin 适合判断普通 HTTP(S) 地址,不能代替对文档创建方式、沙箱和继承关系的分析。
3. 加载、发送与读取
3.1 同源策略具体限制什么
同源策略不是一个对所有请求一视同仁的开关。下面以没有其他额外策略阻止的页面为前提:
| 操作 | 跨源时的主要边界 |
|---|---|
fetch、XHR 读取响应 | 需要满足 CORS 等规则,否则脚本无法取得正常响应 |
| 跨源 iframe 显示页面 | 可以被允许,但父页不能任意读取子页 DOM |
<img> 显示图片 | 可以被允许,但像素读取另有约束 |
普通 <script src> | 可以加载并在当前页面执行,仍受 CSP、MIME 等条件限制 |
模块 <script type="module"> | 跨源加载需要 CORS |
| 外部样式表生效 | 可以被允许,但读取跨源样式表的 cssRules 受限制 |
| Web Storage、IndexedDB | 按源隔离,浏览器隐私分区还可能附加条件 |
| 链接导航、表单提交 | 许多跨源操作可以发起,仍受沙箱和其他策略限制 |
这说明“网页能看到某张图片”和“网页脚本能读取图片内容”是两件事。CSP、混合内容限制、CORP、COEP 等机制还可能进一步限制加载,不能仅凭同源策略推断某个资源必然成功。
3.2 CORS 失败不等于没有请求
假设页面位于 https://app.example.com,接口允许网络连接,但响应没有向该页面授权跨源读取:
async function readProfile() {
try {
const response = await fetch('https://api.example.com/profile');
console.log(await response.text());
} catch (error) {
console.log(error.name); // 此条件下通常是 TypeError
}
}
readProfile();
这个普通 GET 可以已经到达服务端,浏览器在检查响应后才拒绝把结果交给脚本。前端看到的 TypeError 也可能来自其他网络失败,不能只根据异常名称确定根因,应结合控制台、网络面板和服务端记录判断。
某些跨源请求会先发送 预检请求,即浏览器用 OPTIONS 询问服务器是否允许计划中的方法和请求头。预检不通过时,实际请求可以根本没有发出。于是排查时要分别看 OPTIONS 和实际请求,不能一概说“跨域发生在发送前”或“发生在响应后”。
允许读取响应不等于业务操作获准。CORS 是浏览器执行的读取授权机制,不是服务端接口鉴权协议,也不能靠它防住所有跨站请求伪造。
3.3 no-cors 响应为什么不能使用
no-cors 是受限的请求模式,不是关闭浏览器安全检查。跨源成功返回时,脚本通常得到 不透明响应(opaque response):不能读取真实状态、响应头或正文。
async function inspectOpaqueResponse() {
const response = await fetch('https://api.example.com/profile', {
mode: 'no-cors',
});
console.log(response.type, response.status, response.ok);
console.log(await response.text());
// opaque 0 false
// 空字符串,不是接口的真实响应正文
}
inspectOpaqueResponse();
这里的状态 0 不是服务器返回了 HTTP 0;ok 为 false 也不表示服务端必然失败。浏览器只是没有向脚本暴露真实响应。该模式还限制可用方法和请求头,不能用来完成任意跨源 JSON API 调用。
4. 外部脚本与图片
4.1 第三方脚本在哪里执行
普通外部脚本由页面主动引入后,在这个页面的执行环境中运行。脚本下载地址的源,并不会为脚本创建一个隔离的页面环境。
<!-- 页面位于 https://app.example.com -->
<script src="https://cdn.example.net/library.js"></script>
<script type="module" src="https://cdn.example.net/module.js"></script>
如果 library.js 中执行 document.title,访问的是 app.example.com 页面的标题。它也能访问这个页面有权访问的 DOM 和存储。因此,引入第三方普通脚本本身就是信任决定。把不可信脚本移到 CDN,并不会让它失去页面权限。
module.js 的跨源获取需要服务器满足 CORS,即使同地址的普通脚本能加载,也不能推断模块脚本同样能加载。模块语义与加载规则见 ES Module 与 CommonJS。
JSONP 利用了普通脚本的跨源加载方式:服务端把数据包装成预先约定的回调调用,浏览器加载后执行该脚本。它不是把任意接口响应变成可读 JSON,而是要求服务端主动提供这种脚本响应。它通常依赖 GET,不能像 Fetch 一样自由设置请求方法和请求头,并且必须信任服务端返回的代码。新接口通常优先采用 CORS 或受控的服务端转发。
4.2 图片显示与画布读取
未经 CORS 授权的跨源图片可以显示,也可以绘制到画布,但会让画布失去可安全读取像素的状态。后续 getImageData、toDataURL 等读取或导出操作会抛出 SecurityError。
<canvas id="preview" width="40" height="40"></canvas>
<script>
const image = new Image();
image.onload = () => {
const canvas = document.querySelector('#preview');
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0, 40, 40);
try {
console.log(canvas.toDataURL());
} catch (error) {
console.log(error.name); // 未经 CORS 授权的跨源图像:SecurityError
}
};
image.src = 'https://images.example.net/photo.png';
</script>
需要读取像素时,通常要在设置 src 前设置 image.crossOrigin = 'anonymous',同时让图片服务器返回允许该源读取的 CORS 响应。只设置前端属性而服务器不配合,会使加载失败;给已经污染的画布再设置该属性,也不能恢复先前数据的读取权限。
5. iframe 与窗口通信
5.1 嵌入不等于读取
跨源 iframe 可以显示另一个网站,但父页不能因此取得子页的文档对象:
<script>
const frame = document.createElement('iframe');
frame.addEventListener('load', () => {
console.log(frame.contentDocument); // 跨源时为 null
try {
console.log(frame.contentWindow.document);
} catch (error) {
console.log(error.name); // SecurityError
}
}, { once: true });
frame.src = 'https://widget.example.net/';
document.body.append(frame);
</script>
contentWindow 仍可得到受限的窗口引用,从而调用 postMessage 等明确允许的接口,但不能任意读取 document、变量或调用子页函数。给 iframe 文档的 HTTP 响应加上 CORS 头,也不会开放这些 DOM 权限。
“能不能嵌入”还有单独的规则。子页面可以通过响应头 Content-Security-Policy: frame-ancestors 'none' 禁止任何祖先页面嵌入;X-Frame-Options 也用于限制嵌入。父页面的 frame-src 则限制自己可以加载哪些框架。这些限制不能用 CORS 响应头替代。
5.2 使用 postMessage 交换限定的数据
postMessage 是窗口之间异步传递消息的接口。它允许双方交换约定的数据,不开放对方的整个 DOM。消息通过结构化克隆机制传递,普通对象可以传输,函数和 DOM 节点不能直接作为消息内容。
下面由 https://app.example.com 创建受信任组件页面,只接受该 iframe 对本次请求的文本回复:
<output id="result"></output>
<script>
function mountWidget() {
const output = document.querySelector('#result');
const frame = document.createElement('iframe');
const widgetOrigin = 'https://widget.example.net';
const requestId = crypto.randomUUID();
const controller = new AbortController();
let settled = false;
window.addEventListener('message', (event) => {
if (event.origin !== widgetOrigin || event.source !== frame.contentWindow) return;
const data = event.data;
if (!data || typeof data !== 'object' || Array.isArray(data)) return;
if (settled || data.type !== 'widget-result' || data.requestId !== requestId) return;
if (typeof data.text !== 'string' || data.text.length > 200) return;
settled = true;
output.textContent = data.text;
}, { signal: controller.signal });
frame.addEventListener('load', () => {
frame.contentWindow.postMessage({ type: 'widget-request', requestId }, widgetOrigin);
}, { once: true, signal: controller.signal });
frame.src = widgetOrigin + '/widget.html';
document.body.append(frame);
return () => {
controller.abort();
frame.remove();
};
}
const disposeWidget = mountWidget();
// 页面组件卸载时调用 disposeWidget()。
</script>
组件页面 https://widget.example.net/widget.html 对请求方也作检查:
<p>组件页面</p>
<script>
const parentOrigin = 'https://app.example.com';
window.addEventListener('message', (event) => {
if (event.origin !== parentOrigin || event.source !== window.parent) return;
const data = event.data;
if (!data || typeof data !== 'object' || Array.isArray(data)) return;
if (data.type !== 'widget-request') return;
if (typeof data.requestId !== 'string' || data.requestId.length === 0 || data.requestId.length > 64) return;
event.source.postMessage({
type: 'widget-result',
requestId: data.requestId,
text: '已连接',
}, parentOrigin);
});
</script>
检查源,是为了确认发送页面所属的信任范围;检查 source,是为了确认消息来自预期窗口;检查类型、长度和请求标识,是为了只处理当前协议允许的消息。同源的另一个 iframe 也不自动成为本次请求的合法回复方。
发送时的 targetOrigin 会限制哪种源的目标页面可以收到消息,降低目标窗口导航后被错误接收的风险。它不是路径过滤,也不替代接收方检查。event.origin 描述发送消息时的源,不能证明那个窗口此后永远没有导航。
示例只展示一次请求和回复,不代表业务授权完成。真实组件还要处理等待超时、导航、错误结果和重复请求。传给 textContent 的文本按文本展示;如果改成 innerHTML,消息内容又会进入 HTML 解析流程,需要另外处理安全边界。
5.3 sandbox 可以进一步收紧权限
iframe 的 sandbox 属性会增加限制,再通过选项开放需要的能力。例如只允许执行脚本,并不保留文档通常拥有的源:
<iframe
sandbox="allow-scripts"
srcdoc="<script>parent.postMessage({ type: 'sandbox-ready' }, '*')</script>"
></iframe>
这个示例没有 allow-same-origin,所以子文档具有不透明源,父页收到消息时 event.origin 为字符串 null。这里向父页发送的只是无敏感信息的演示通知;真实协议仍应收紧发送目标,并验证窗口身份和消息内容,不能把 null 当作某个可信网站的名字。
allow-same-origin 表示不因这项沙箱规则强制改成不透明源,并不表示允许任意跨源 DOM 访问。对与父页同源的不可信内容,同时开放 allow-scripts 和 allow-same-origin 可能让子页移除自身的沙箱限制,不应依赖这种组合来隔离它。需要脚本能力的不可信内容应使用独立源,并谨慎开放必要能力。
6. 跨源通信方案的分工
选择方案前,先说清交换的是接口响应、窗口消息还是资源文件。
| 场景 | 合适的方向 | 不能据此推断的权限 |
|---|---|---|
| 页面读取自己控制的跨源 API | 服务端按需要配置 CORS | 不开放 iframe DOM,也不替代身份校验 |
| 页面调用同源后端,由后端访问其他服务 | 受控的后端转发或反向代理 | 不是允许任意用户指定任意目标地址的开放代理 |
| 两个页面或 iframe 交换业务消息 | 约定 postMessage 协议 | 不直接共享变量、DOM 或信任全部消息 |
| 加载外部图片、脚本等资源 | 对应的资源元素和加载规则 | 不自动得到资源正文或像素的读取权 |
| 兼容既有 JSONP 接口 | 受信任脚本回调 | 不支持任意请求方法,也没有普通 JSON 响应的隔离性 |
开发服务器代理会让浏览器访问本地同源路径,再由开发服务器转发,因此本地不再出现相同的跨源读取问题。部署时若没有相应的生产路由或 CORS 配置,问题仍会出现。服务器之间的 HTTP 客户端通常不执行浏览器的同源策略,但仍受网络、权限和应用安全约束。
不要通过关闭浏览器安全检查验证“线上已经修好”。也不应把已废弃的 document.domain 放宽方式作为新系统方案:它只针对特定旧式文档访问场景,不能统一放开 Fetch、存储和现代隔离机制。
7 · 面试题卡片
本页题目与参考回答
7.1 什么是同源,子域名和不同端口是否同源?
考察重点:源的三元组、默认端口、源与站点的区别。
相关内容:普通地址的同源判断。
参考回答
普通 HTTP(S) URL 的协议、主机和有效端口都相同才同源。 子域名不同或非默认端口不同都会跨源;路径、查询参数和片段不参与判断。
HTTPS 默认端口 443 与显式 :443 等价。两个域名解析到同一 IP 不会因此同源;同站也不等于同源。
7.2 浏览器为什么需要同源策略,它能代替服务端鉴权吗?
考察重点:浏览器中的信任边界、读取授权与业务授权。
相关内容:为什么要隔离读取。
参考回答
它防止一个网站的脚本随意读取另一个源的数据,不能代替服务端鉴权。 用户同时打开多个网站,登录了其中一个网站,不意味着其他页面有权读取其私有页面和响应。
部分跨站写操作仍可以发起,是否携带凭据还有独立规则。因此服务端必须继续检查身份、资源权限和有副作用请求的安全条件。
7.3 同源策略限制哪些操作,跨域错误是否表示请求没有发出?
考察重点:跨源读取、嵌入、存储与请求阶段的区别。
相关内容:同源策略具体限制什么、CORS 失败不等于没有请求。
参考回答
主要限制包括跨源 DOM、接口响应和源隔离存储的读取,不能把它理解为禁止所有跨域请求。 图片、普通外部脚本、iframe 嵌入和导航都有各自规则。
普通跨源 GET 可能已经到达服务端,只是响应未获读取授权;需要预检的请求则可能在预检失败后不再发送实际请求。要结合实际请求、OPTIONS 和服务器记录判断。
7.4 为什么页面可以使用跨域 CDN 上的 JavaScript 库?
考察重点:普通脚本加载例外、执行环境、模块加载差异。
相关内容:第三方脚本在哪里执行。
参考回答
普通外部脚本允许跨源加载,并在引入它的页面环境里执行。 这不是让脚本获得 CDN 页面的数据,而是页面主动把自己的脚本权限交给了引入的代码,所以要信任脚本提供方。
这种加载仍受 CSP、MIME 等规则影响;模块脚本跨源加载需要 CORS,不能直接套用普通脚本的结论。
7.5 跨域图片显示正常,为什么导出 canvas 却失败?
考察重点:资源显示与像素读取、画布污染。
相关内容:图片显示与画布读取。
参考回答
显示许可不等于读取像素的许可,未经 CORS 授权的跨源图像会污染画布。 绘制后调用 toDataURL 或 getImageData 会抛出 SecurityError。
应在加载前设置合适的 crossOrigin,并由图片服务器返回相应授权。只改前端属性不能替代服务器配合,也不能恢复已经污染的数据。
7.6 跨源 iframe 有哪些限制,给响应加 CORS 头能读取其 DOM 吗?
考察重点:窗口对象访问、嵌入策略与网络响应授权的分工。
相关内容:嵌入不等于读取、sandbox 可以进一步收紧权限。
参考回答
父页不能任意读取跨源 iframe 的 DOM,CORS 头不会开放这种权限。 contentDocument 为 null,读取受限窗口的 document 会抛出安全异常,但可以使用允许的 postMessage 接口。
能否嵌入还受 frame-ancestors、X-Frame-Options、父页 frame-src 和 sandbox 等规则影响。这些规则与脚本能否读取响应是不同问题。
7.7 使用 postMessage 时为什么既校验 origin,又校验 source 和数据?
考察重点:发送方身份、预期窗口、消息协议、目标导航。
相关内容:使用 postMessage 交换限定的数据。
参考回答
三种检查回答不同问题:发送方属于哪个源、是不是预期窗口、内容是否符合当前协议。 仅检查源,无法排除同源的其他窗口;仅检查窗口,不能忽略它可能发生导航;仅检查身份,也不能假设数据类型和请求标识一定正确。
发送方应指定准确的 targetOrigin,接收方检查 origin、source、消息结构及请求关联,按文本展示普通字符串,并在组件卸载后清理监听。
7.8 把 fetch 改成 no-cors,为什么还是读不到 JSON?
考察重点:不透明响应与真实 HTTP 响应的区别。
相关内容:no-cors 响应为什么不能使用。
参考回答
no-cors 不会开放跨源读取,成功请求通常只得到不可读取正文的不透明响应。 脚本看到的状态为 0、ok 为 false,text() 得到空字符串,不能据此判断服务端真实状态。
需要读取 API 结果,应使用服务器允许的 CORS 或合适的同源后端转发,而不是依赖这个请求模式绕过授权。
7.9 跨域通信有哪些方式,应该如何区分使用场景?
考察重点:接口调用、窗口消息、资源加载和代理的职责。
相关内容:跨源通信方案的分工。
参考回答
先区分要交换什么:接口响应使用 CORS 或受控代理,窗口间交换业务数据使用 postMessage,图片和脚本按各自资源规则加载。 它们没有一个统一的“解除所有跨域限制”的效果。
开发代理只改变开发环境的请求路径,部署仍要有对应方案。服务端转发需要控制目标地址和权限;窗口消息需要验证身份和协议;跨源资源加载也不自动授予正文、像素或 DOM 的读取权限。
7.10 JSONP 的原理和不足是什么?
考察重点:脚本回调与 JSON 数据、请求限制和执行信任。
相关内容:第三方脚本在哪里执行。
参考回答
JSONP 让服务器返回回调函数调用,再由普通 script 跨源加载并执行。 服务端必须配合这种响应格式,不能直接把任意 JSON 接口变成 JSONP。
它通常依赖 GET,不能自由控制请求方法和请求头;返回的是会执行的代码,必须信任服务端。新接口更适合通过 CORS 或受控后端转发交换数据。
7.11 两个沙箱 iframe 的 origin 都显示 null,它们同源吗?
考察重点:不透明源与源字符串、sandbox 的权限边界。
相关内容:不透明源与继承的源、sandbox 可以进一步收紧权限。
参考回答
不因此同源,null 是不透明源的序列化结果,不是所有这些文档共同使用的可信源名称。 缺少 allow-same-origin 的独立沙箱文档可以拥有各自的不透明源。
不能只凭 event.origin === 'null' 信任消息。应根据实际通信设计验证窗口身份和协议,也不能把 allow-same-origin 误解为允许任意跨域访问。
8. 参考资料
- WHATWG HTML:Origins
- MDN:Same-origin policy
- WHATWG Fetch:Responses 与 CORS protocol
- WHATWG HTML:The script element
- WHATWG HTML:Security with canvas elements
- WHATWG HTML:Cross-document messaging
- MDN:Window.postMessage
- WHATWG HTML:The iframe element 与 sandbox
- MDN:CSP frame-ancestors
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明