前言
以往也了解过同源策略、跨域等方面的知识,但都是零零散散的,也没深入思考相互之间的联系,本文旨在通过将相关的知识点串联起来,系统性的归纳总结。
一、同源策略是什么?
同源策略限制了从同一个源加载的文档或脚本如何与来自另外一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
1.1 怎么才算是同源?
同时满足以下3个条件:
- 协议
http 和 https 会被算作不同 - 域名
注意二级域名和主域名会被算作不同 - 端口
1.2 同源策略具体限制了什么
- 无法读取非同源网页的 Cookie、LocalStorage、IndexedDB
- 无法接触非同源网页的 DOM
- 向非同源地址发送 AJAX 请求,浏览器会拦截请求的响应。
二、为什么要有同源策略?
试想一下,如果没有同源策略,会发生什么?
随便举几个例子,如果没有 1.2 中的种种限制,下面的情况很容易发生:
- 嵌套第三方网页,并修改第三方网页的 DOM 结构
- xss攻击难度降低
在有同源策略之后,需要向被攻击站点注入脚本才能获取 LocalStorage、Cookie 中的信息,如果没有同源限制,只要被攻击者访问过其他站点,再访问攻击者的站点,攻击者站点中的脚本可直接获取被攻击者之前访问过的站点的信息了,都不需要注入脚本了。 - csrf 攻击
既然能轻易获取到 cookie 等信息,可以直接通过 AJAX 发起伪造请求了。
通过以上 3 个例子可以看出,安全隐患太严重了,必须要引入同源策略来规避风险。
三、同源策略带来的问题
事物具有两面性,同源策略降低了安全风险的同时,也带来了很多不便。
再举几个例子:
- 前后端分离时期的 AJAX 跨域请求
- 新站点嵌套了部分旧站点页面,需要通信
不同源的站点或地址之间的通信被阻断,即常见的跨域问题。
四、如何解决跨域
首先说明一点,跨域不仅仅包括 ajax 请求这一种情况,不同源的网页之间也有通信的需要。
(一) AJAX
4.1 JSONP
原理:
-
<script>
标签的 src 属性是不受同源限制的(<img>
标签也是如此) - 先提前定义好一个回调函数
function foo(data) {
console.log(data);
}
- 将 foo 当做请求参数传递到服务器
- 服务器解析参数,获取 foo
- 服务器从数据库获取业务数据 data
- 服务器返回响应 foo(data) , 即把 data 当做参数
- 因为是通过 <script> 请求的,所以得到的响应就相当于请求回来一段 js 脚本,然后执行。
缺点:只支持 GET
请求。
4.2 CORS
CORS 全称是跨域资源共享(Cross-origin resource sharing)
详细请查看我的另一篇文章 CORS 学习笔记
4.3 Access-Control-Allow-Origin 白名单
其实就是 CORS 的具体实现。
服务端设置 header 头
Access-Control-Allow-Origin: *;
一般开发平台提供的 API 都是这样做的。但是对于不想开放的平台,使用 '*' 存在较大的安全风险,建议使用精确的域名来控制。
额外解答:为什么 Access-Control-Allow-Credentials: true;
时,不能设置 Access-Control-Allow-Origin
的值为 *
。
我的思考结论:Access-Control-Allow-Credentials
字段用来控制对带凭证的请求,服务器是否返回响应,Access-Control-Allow-Origin
字段用来控制请求的资源是否允许访问,我们先假设允许问题中的设置,会出现什么情况呢?会出现:一个携带凭证的跨域请求可以得到请求结果,猛一看好像没啥问题啊,我们平时开发不就为了实现跨域请求嘛,可是如果这个请求是伪造的呢?即 csrf 攻击,因为 Access-Control-Allow-Origin: *
所以同源策略中对于不同源 AJAX 请求的限制没了,这很容易造成安全问题。这里细心的同学会发现,那和 Access-Control-Allow-Credentials: true;
有什么关系呢?我的理解是,为什么请求需要带凭证呢,或者说什么样的请求需要带凭证?这样想就会明白,肯定是重要的资源,有安全要求,不想轻易被访问,所以服务器会对凭证进行校验,校验通过后才会返回响应的,因为像是开发平台这样的站点就是让别人请求的,所以设置Access-Control-Allow-Origin: *
不会有问题,核心思想就是“重要的资源,不应该允许任意站点请求”,这个问题解释完了。
4.4 WebSocket
webSocket 是不受同源限制的。
4.5 代理服务器
浏览器请求同源服务器,服务器负责请求外部服务器。
举例:nginx 反向代理
(二) Cookie
4.6 设置 cookie 的 domain 属性
cookie 有个 domain
属性,用来设置 cookie 的作用域(结合 cookie 的另一个属性 path
使用)。
如果子域名 a.mozilla.org 和 b.mozilla.org 想要共享一段 cookie ,只要将 cookie 的 domain
属性设置成共同的父域名 domain=mozilla.org 即可。
4.7 document.domain
这里阮一峰的方法是在两个子站点中通过脚本设置 document.domain='mozilla.org'
。
缺点:
- 不方便,因为需要协作的站点都要执行上述脚本才行,原因:
document.domain = xxx
会导致端口号被重置为null。 - 不适合只需要共享部分 cookie 的需求
优点:document.domain 可以达到让父子域或者多个子域变成同源,作用不局限于 cookie 的共享。
(三) Iframe 跨窗口通信
4.8 片段识别符
指 url 中 #
后面的部分, 把需要传递的数据放到片段识别符中。
父窗口改变子窗口的片段标识符:
let src = originURL + '#' + data;
document.getElementById('childIFrame').src = src;
子窗口改变父窗口的片段标识符:
parent.location.href = target + '#' + hash;
监听 hash 的变化
window.onhashchange = function () {
let msg = window.location.hash;
}
缺点:
- url 长度有限,不能传递太多的数据
- 不是标准规范
4.9 window.postMessage()
跨文档通信 API(Cross-document messaging)为 window 对象提供了postMessage(),允许跨窗口通信,无论是否同源。
语法:otherWindow.postMessage(message, targetOrigin, [transfer]);
父窗口向子窗口发送消息:
let pop = window.open('http://bbb.com', 'title');
pop.postMessage('hello world', 'http://bbb.com');
子窗口向父窗口发送消息:
window.opener.postMessage('I received', 'http://aaa.com');
监听消息:
window.addEventListener('message', function(e) {
console.log(e.data);
})
- event.source:发送消息的窗口
- event.origin: 消息发向的网址
- event.data: 消息内容
安全
- 不希望从其他网站接收 message,请不要为 message 添加事件监听。
- 始终使用
origin
和source
属性验证发件人的身份。 - 向其他窗口发送数据时,指定精确的
origin
。
参考
阮一峰:同源策略
阮一峰:CORS
会编程的银猪:同源策略和跨域请求研究
MDN:Same-origin_policy
MDN:postMessage
MDN:CORSNotSupportingCredentials
MDN:Access-Control-Allow-Credentials