摘要:同源策略年,同源政策由公司引入浏览器。标签不受同源策略限制,但只能发起请求。这一行为使得不同域的特定文档可以读取该属性值,因此可以绕过同源策略并使跨域消息通信成为可能。
前言
现在cross-origin resource sharing(跨域资源共享,下简称CORS)已经十分普及,算上IE8的不标准兼容(XDomainRequest),各大浏览器基本都已支持,当年为了前后端分离、iframe交互和第三方插件开发而头疼跨域是时代已经过去,但当年为了跨域无所不用其极的风骚操作却依然值得学习。
本篇文章不是从实用的角度考量这些旧时代的跨域手段,而是更偏向理论的阐述,并引发对浏览器安全的思考,因为跨域实际上也是各类攻击的核心。
本人个人能力有限,欢迎大牛一起讨论,批评指正。
1995年,同源政策由Netscape公司引入浏览器。目前,所有浏览器都实行这个安全策略。
核心是确保不同源提供的文件(资源)之间是相互独立的。换句话说,只有当不同的文件脚本是由相同的域、端口、HTTP协议提供时,才没有特殊的限制。特殊限制可以细分为两个方面:
对象访问限制:主要体现在iframe,如果父子页面的源是不同的,那就不可以访问对方的DOM方法和属性(包括Cookie、LocalStorage和IndexDB等)。不同来源便抛出异常。
网络访问限制:主要体现在AJAX请求,如果发起的请求目标源与当前页面不同,浏览器就会限制了发起跨站请求,或拦截返回的请求。
一个表格看懂什么是同源?
origin(URL) | result | reason |
---|---|---|
http://example.com | success | 协议,域名和端口号80均相同 |
http://example.com:8080 | fail | 端口不同 |
https://example.com | fail | 协议不同 |
http://sub.example.com | fail | 域名不同 |
至于为什么说这是个安全策略?
这个就要提到cookie-session机制,众所周知HTTP是无状态协议,而服务器如何知晓用户的登录状态?传统上是使用了cookie-session这一机制,也就是服务器为每个访问者生成了一个session标识,而session标识会被服务器包含在应答头中返回,浏览器解析到应答头中的set-cookie就把这串session标识保存到本地cookie中,利用cookie每次请求同一个域都会带上的特性,服务器器就能知晓当前的用户登录状态。
所以如果让浏览器向不同源发起请求,就会造成很大的危险。比如用户登录了银行的网站A,也就是说A站已经在浏览器留下了cookie,这时候用户又访问了B站,如果能在B站页面上发起A站的请求,就相当于B站可以冒充用户,在A站为所欲为。
由此可见,"同源策略"是必需的,否则cookie可以共享,互联网就毫无安全可言了。
同源策略提出的时代还是传统MVC架构(jsp,asp)盛行的年代,那时候的页面靠服务器渲染完成了大部分填充,内容也比较简单,开发者也不会维护独立的API工程,所以其实跨域的需求是比较少的。
新时代前后端的分离和第三方JSSDK的兴起,我们才开始发现这个策略虽然大大提高了浏览器的安全性,但有时很不方便,合理的用途也受到影响。比如:
独立的API工程部署为了方便管理使用了独立的域名;
前端开发者本地调试需要使用远程的API;
第三方开发的JSSDK需要嵌入到别人的页面中使用;
公共平台的开放API。
于是乎,在没有标准规范的时代,如何解决这些问题的跨域方案就被纷纷提出,可谓百家争鸣,其中不乏令人惊叹的骚操作,这样的极客精神依然值得我们敬佩和学习。
JSON-PJSON-P是各类跨域方案中流行度较高的一个,现在在某些要兼容旧浏览器的环境下还会被使用,著名的jQuery也封装其方法。请勿见名知义,名字中的P是padding“带填充”的意思,这个方法在通信过程中使用的并不是普通的json,而是自带填充功能的JavaScript脚本。
如何理解“自带填充功能的JavaScript脚本”,看看下面的例子或许比较简单,如果一个js文件里这样写并被引入,则全局下就会有data对象,也就是说利用js脚本的引入和解析可以用来传递数据,如果把js脚本换成函数运行命令岂不是可以调用全局函数了。这就是JSON-P方法的核心思想,它填充的是全局函数的数据。
var data = { a: 1, b: 2 }
【PS】标签不受同源策略限制,但只能发起get请求。
原理及流程
先定义好回调函数,也就是引入的js脚本中要调用的函数;
新建标签,将标签插入页面浏览器便会发起get请求;
服务器根据请求返回js脚本,其中调用了回调函数。
// 定义回调函数 function getTheAnimal(data){ var myAnimal = data.animal; } // 新建标签 var script = document.createElement("script"); script.type = "text/javascript"; // 常用的在url参数部分跟服务器约定号回调函数名 script.src = "http://demo.com/animal.json?callback=getTheAnimal"; document.getElementByTagName("head")[0].appendChild(script);
总结
优点:
简单,有现成的工具库(jQuery)支持;
支持上古级别的浏览器(IE8-)。
缺点:
只能是GET方法;
受浏览器URL最大长度2083字符限制;
无法调试,服务器错误无法检测到具体原因;
有CSRF的安全风险;
只能是异步,无法同步阻塞;
需要特殊接口支持,不能基于REST的API规范。
子域名代理这个方法实际上是利用浏览器允许iframe内的页面只要是跟父页面是同个一级域名下,就能被父页面修改和调用的特点。也许你会疑问,上面讲同源策略的表格中很明确二级域名不同也是算不同源,这岂不矛盾了?
这其实不矛盾,如果正常操作确实会被同源策略限制,但浏览器的document.domain允许网站将主机部分更改为原始值的后缀。这意味着,寄放在sub.example.com的页面可以将它的源设置为example.com,但是并不能将其设置为alt.example.com或google.com。
【PS】这里有一个细节,父子页面均要设置document.domain才能被互相访问,单一一个是无法跨域的。document.domain的特点:只能设置一次;只能更改域名部分,不能修改端口号和协议;重置源的端口为协议默认端口。
原理及流程
新建一个子域,比如api.demo.com(页面在主域名demo.com下);
子域下需要一个代理文件proxy.html,设置其document.domain = "demo.com",并可以包含发起ajax的工具;
所有API地址都是在api.demo.com;
把需要发请求的主域页面设置其document.domain = "demo.com";
新建iframe标签链接到代理页;
当iframe内的子页面就绪时,父页面就可以使用子页面发起ajax请求。
// 最简单的代理文件proxy.html
// 新建iframe var iframe = document.createElement("iframe"); // 链接到代理页 iframe.src = "http://api.demo.com/proxy.html"; // 代理页就绪时触发 iframe.onload = function(){ // 由于代理页已经和父页设置了相同的源,父的脚本可以调用代理页的ajax工具; // 由于是在子页面发起,其请求地址就跟子页面同源了。 iframe.contentWindow.jQuery.ajax({ method: "POST", url: "http://api.demo.com/products", data: { product: id, }, success: function(){ document.body.removeChild(iframe); /*...*/ } }) } document.getElementsByTagName("head")[0].appendChild(iframe);
总结
优点:
可以发送任意类型的请求;
可以使用基于REST的API规范。
缺点:
不太适合第三方API,给第二方使用较麻烦;
iframe对浏览器性能影响较大;
无法使用非协议默认端口的API。
模拟form表单form表单的target属性可以指定一个iframe,使主页面不跳转,而iframe内跳转,所以这个方法的核心就是利用表单提交,并在iframe中获取数据。
要访问iframe内外页面互访也是必须设置同源,这点与子域代理是相似的;而iframe内回调父页面,又与JSON-P相似,可以说是两个思路的合体版。
form表单提交后返回的是页面,所以与JSON-P不同的是,返回的是包含了自带填充功能的JavaScript脚本的页面,说起来有点绕,简单来说就是把JSON-P返回的脚本放到一个html页面里自运行。
相比子域代理的方法,它不需要代理页。
【PS】form表单提交的特点就是会导致整个页面跳转,返回数据是在新的页面上,这样自然不会产生跨域的问题。
原理及流程
新建一个子域,比如api.demo.com(页面在主域名demo.com下);
所有API地址都是在api.demo.com;
把需要发请求的主域页面设置其document.domain = "demo.com";
先定义好父页面上的回调函数;
新建iframe标签并指定名字;
新建表单form标签,指定target为刚才的iframe,并添加数据;
提交表单,iframe内跳转,其中自运行脚本调用了父页面的回调函数。
// 新建并隐藏iframe var frame = document.createElement("iframe"); iframe.name = "post-review"; frame.style.display = "none"; // 新建表单 var form = document.createElement("form"); form.action = "http://api.demo.com/products"; form.method = "POST"; form.target = "post-review"; // 添加数据 var score = document.createElement("input"); score.name = "score"; score.value = "5"; // 添加数据 var message = document.createElement("input"); message.name = "message"; message.value = "hello world"; // 把数据加到表单 form.appendChild(score); form.appendChild(message); // 渲染iframe和表单 document.body.appendChild(frame); document.body.appendChild(form); // 提交表单发起请求 form.submit(); // 完成清理元素 document.body.removeChild(form); document.body.removeChild(frame);
// 最简单返回html
总结
由于这个方法是JSON-P与子域名代理的结合版,可以说即拥有两者的优点,也保留了两者一些缺点。
优点:
可以发送任意类型的请求;
不需要代理页;
支持上古级别的浏览器(IE8-)。
缺点:
不太适合第三方API,给第二方使用较麻烦;
iframe对浏览器性能影响较大;
无法使用非协议默认端口的API;
需要特殊接口支持,不能基于REST的API规范。
window.name这方法利用了window.name的特性:一旦被赋值后,当窗口被重定向到一个新的URL时不会改变它的值。这一行为使得不同域的特定文档可以读取该属性值,因此可以绕过同源策略并使跨域消息通信成为可能。
【PS】例子里演示的是发起get请求,只要把请求地址直接写到src里就行了。如果想要发起其他类型的请求,可以类比采用模拟的form的方式进行改造。
原理及流程
新建iframe,使用iframe访问一个非同源的地址(发请求);
当页面加载完成后,iframe内脚本给window.name属性赋值,这时父页面还是不能读取到子页面的属性(因为不同源);
iframe自身回调到一个同源的地址(可能只是个空白页),这时候window.name没有改变;
父页面顺利读取window.name的值。
// 新建iframe var iframe = document.createElement("iframe"); var body = document.getElementByTagName("body"); // 隐藏iframe并链接地址 iframe.style.display = "none"; iframe.src = "http://api.demo.com/server.html?id=1"; // 因为需要两次跳转,这里有个完成标记 var done = fasle; // 这里会触发至少两次,一次由于非同源是取不到值的。 iframe.onload = iframe.onreadystatechange = function(){ if(! this.readyState && (iframe.readyState !== "complete" || done)){ return; } console.log("Listening"); var name = iframe.contentWindow.name; if(name){ console.log(iframe.contentWindow.name); done = true; } }; body.appendChild(iframe);
// 最简单返回html
总结
优点:
可以发送任意类型的请求;
不需要设置子域名。
缺点:
iframe对浏览器性能影响较大;
需要特殊接口支持,不能基于REST的API规范;
每当你想要获取一条新的消息时都不得不发起两次网络请求,网络成本大;
需要准备空白页,对它的访问是无意义的,影响流量统计。
window.hash这个方法利用了location的特性:不同域的页面,可以写不可读。而只改变哈希部分(井号后面)不会导致页面跳转。也就是可以让父、子页面互相写对方的location的哈希部分,进行通讯。
原理及流程
新建iframe,使用iframe访问一个非同源的地址(发请求),参数里带上父页面url;
当页面加载完成后,iframe内脚本设置父页面的url并在哈希部分带上数据;
父页面的脚本循环检查哈希值的变化,如果检查到有值就取值并清空哈希值;
【PS】父页面会循环检查哈希是否改变来读取值,因为这种降级方案的使用环境一般是不会有hashchange事件的。演示里是最简单的get方法,如果想要发起其他类型的请求,可以类比采用模拟的form的方式进行改造,但记住不要丢失父页面的url。
// 获取当前url var url = window.location.href; // 新建iframe var iframe = document.createElement("iframe"); // 隐藏iframe并设置链接,把当前url带上 iframe.style.display = "none"; iframe.src = "http://api.demo.com/server.html?id=1&url=" + encodeURIComponent(url); var body = document.getElementByTagName("body")[0]; body.appendChild(iframe); // 循环监听处理 var listener = function(){ // 读取 var hash = location.hash; // 还原 if(hash && hash !== "#"){ console.log(hash.replace("#", "")); window.loacation.href = url + "#"; } // 继续监听 setTimeout(listener, 100); }; listener();
// 最简单返回html
总结
优点:
可以发送任意类型的请求;
不需要设置子域名。
缺点:
iframe对浏览器性能影响较大;
需要特殊接口支持,不能基于REST的API规范;
循环检查哈希需要消耗性能;
返回数据受浏览器URL最大长度2083字符限制。
现代的标准W3C的标准化跨域方案,让现代浏览器跨域已经不是什么复杂的事。这部分网上资料已经很多,这里就只是简单介绍。
CORSCORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS参考文档
跨域资源共享 CORS 详解
H5的window.postMessage为浏览器带来了一个安全的。基于事件的消息api。
只要是window对象,基本都可以使用这个方法,也就是说window.name、window.hash这类风骚的操作都已成为降级方案。
postMessage参考文档
安全问题上述的各类非标准的骚操作,都算是对同源策略的破解办法,在方便开发者完成跨域目的的同时,各类恶意的攻击者也自然会利用这些方案为非作歹。
其中子域名代理的风险最低,因为需要服务器设置特定的子域名,也就是已经是两个源的协商结果,一般黑客是难以模拟的。
风险最高的要算JSON-P的方案,因为这是任何客户端都可随意使用的办法,CSRF攻击的核心也是利用了特定标签的跨域性发起请求,所以JSON-P最好用在无用户状态的低安全性API上。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/52138.html
摘要:同源策略年,同源政策由公司引入浏览器。标签不受同源策略限制,但只能发起请求。这一行为使得不同域的特定文档可以读取该属性值,因此可以绕过同源策略并使跨域消息通信成为可能。 前言 现在cross-origin resource sharing(跨域资源共享,下简称CORS)已经十分普及,算上IE8的不标准兼容(XDomainRequest),各大浏览器基本都已支持,当年为了前后端分离、if...
摘要:下面也是以模块的模块集为例,可以发现和路由有一些不同就是这里为了防止模块跟全局耦合,运用函数式编程思想类似于依赖注入,将全局的实例作为函数参数传入,再返回出一个包含的对象,这个导出的对象将会被以模块名命名,合并到全局的集中。 前言 web前端发展到现代,已经不再是严格意义上的后端MVC的V层,它越来越向类似客户端开发的方向发展,已独立拥有了自己的MVVM设计模型。前后端的分离也使前端人...
摘要:下面也是以模块的模块集为例,可以发现和路由有一些不同就是这里为了防止模块跟全局耦合,运用函数式编程思想类似于依赖注入,将全局的实例作为函数参数传入,再返回出一个包含的对象,这个导出的对象将会被以模块名命名,合并到全局的集中。 前言 web前端发展到现代,已经不再是严格意义上的后端MVC的V层,它越来越向类似客户端开发的方向发展,已独立拥有了自己的MVVM设计模型。前后端的分离也使前端人...
摘要:接上篇议题合理的架构讨论上传送门。处理思路如下使用上面定义的方法获取如果能获取到则说明有有效的,则时候即可跳转到目标页如果获取到空字符串,则说明无效或不存在,跳转至登录页面。 接上篇《【Geek议题】合理的VueSPA架构讨论(上)》传送门。 自动化维护登录状态 登录状态标识符跟token类似,都是需要自动维护有效期,但也有些许不同,获取过程只在用户登录或注册的时候,不需要自动获取。 ...
摘要:接上篇议题合理的架构讨论上传送门。处理思路如下使用上面定义的方法获取如果能获取到则说明有有效的,则时候即可跳转到目标页如果获取到空字符串,则说明无效或不存在,跳转至登录页面。 接上篇《【Geek议题】合理的VueSPA架构讨论(上)》传送门。 自动化维护登录状态 登录状态标识符跟token类似,都是需要自动维护有效期,但也有些许不同,获取过程只在用户登录或注册的时候,不需要自动获取。 ...
阅读 1355·2021-10-09 09:44
阅读 1438·2021-09-28 09:36
阅读 15925·2021-09-22 15:55
阅读 1237·2021-09-22 15:45
阅读 2197·2021-09-02 09:48
阅读 2781·2019-08-29 17:19
阅读 2294·2019-08-29 10:54
阅读 904·2019-08-23 18:40