【Geek议题】当年那些风骚的跨域操做

前言

如今cross-origin resource sharing(跨域资源共享,下简称CORS)已经十分普及,算上IE8的不标准兼容(XDomainRequest),各大浏览器基本都已支持,当年为了先后端分离、iframe交互和第三方插件开发而头疼跨域是时代已通过去,但当年为了跨域无所不用其极的风骚操做却依然值得学习。
本篇文章不是从实用的角度考量这些旧时代的跨域手段,而是更偏向理论的阐述,并引起对浏览器安全的思考,由于跨域实际上也是各种攻击的核心。
本人我的能力有限,欢迎大牛一块儿讨论,批评指正。javascript

同源策略

1995年,同源政策由Netscape公司引入浏览器。目前,全部浏览器都实行这个安全策略。
核心是确保不一样源提供的文件(资源)之间是相互独立的。换句话说,只有当不一样的文件脚本是由相同的域、端口、HTTP协议提供时,才没有特殊的限制。特殊限制能够细分为两个方面:html

  • 对象访问限制:主要体如今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能够共享,互联网就毫无安全可言了。java

跨域方案

同源策略提出的时代仍是传统MVC架构(jsp,asp)盛行的年代,那时候的页面靠服务器渲染完成了大部分填充,内容也比较简单,开发者也不会维护独立的API工程,因此其实跨域的需求是比较少的。
新时代先后端的分离和第三方JSSDK的兴起,咱们才开始发现这个策略虽然大大提升了浏览器的安全性,但有时很不方便,合理的用途也受到影响。好比:jquery

  1. 独立的API工程部署为了方便管理使用了独立的域名;
  2. 前端开发者本地调试须要使用远程的API;
  3. 第三方开发的JSSDK须要嵌入到别人的页面中使用;
  4. 公共平台的开放API。

因而乎,在没有标准规范的时代,如何解决这些问题的跨域方案就被纷纷提出,可谓百家争鸣,其中不乏使人惊叹的骚操做,这样的极客精神依然值得咱们敬佩和学习。ajax

JSON-P

JSON-P是各种跨域方案中流行度较高的一个,如今在某些要兼容旧浏览器的环境下还会被使用,著名的jQuery也封装其方法。请勿见名知义,名字中的P是padding“带填充”的意思,这个方法在通讯过程当中使用的并非普通的json,而是自带填充功能的JavaScript脚本
如何理解“自带填充功能的JavaScript脚本”,看看下面的例子或许比较简单,若是一个js文件里这样写并被引入,则全局下就会有data对象,也就是说利用js脚本的引入和解析能够用来传递数据,若是把js脚本换成函数运行命令岂不是能够调用全局函数了。这就是JSON-P方法的核心思想,它填充的是全局函数的数据。json

var data = {
  a: 1,
  b: 2
}
【PS】 <script>标签不受同源策略限制,但只能发起get请求。

原理及流程后端

  1. 先定义好回调函数,也就是引入的js脚本中要调用的函数;
  2. 新建<script>标签,将标签插入页面浏览器便会发起get请求;
  3. 服务器根据请求返回js脚本,其中调用了回调函数。

jsonp流程图

// 定义回调函数
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);

总结 api

优势:跨域

  • 简单,有现成的工具库(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的特色:只能设置一次;只能更改域名部分,不能修改端口号和协议;重置源的端口为协议默认端口。

原理及流程

  1. 新建一个子域,好比api.demo.com(页面在主域名demo.com下);
  2. 子域下须要一个代理文件proxy.html,设置其document.domain = 'demo.com',并能够包含发起ajax的工具;
  3. 全部API地址都是在api.demo.com;
  4. 把须要发请求的主域页面设置其document.domain = 'demo.com'
  5. 新建iframe标签连接到代理页;
  6. 当iframe内的子页面就绪时,父页面就可使用子页面发起ajax请求。

子域名代理流程图

// 最简单的代理文件proxy.html
<!DOCTYPE html>
<html>
    <script>
        document.domain = 'demo.com';
    </script>
    <script src="jquery.min.js"></script>
</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表单提交的特色就是会致使整个页面跳转,返回数据是在新的页面上,这样天然不会产生跨域的问题。

原理及流程

  1. 新建一个子域,好比api.demo.com(页面在主域名demo.com下);
  2. 全部API地址都是在api.demo.com;
  3. 把须要发请求的主域页面设置其document.domain = 'demo.com'
  4. 先定义好父页面上的回调函数;
  5. 新建iframe标签并指定名字;
  6. 新建表单form标签,指定target为刚才的iframe,并添加数据;
  7. 提交表单,iframe内跳转,其中自运行脚本调用了父页面的回调函数。

模拟form表单流程图

// 新建并隐藏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
<!DOCTYPE html>
<html>
    <script>
        document.domain = 'demo.com';
        window.parent.jsonpCallback('{"status":"success"}');
    </script>
</html>

总结

因为这个方法是JSON-P与子域名代理的结合版,能够说即拥有二者的优势,也保留了二者一些缺点。

优势:

  • 能够发送任意类型的请求;
  • 不须要代理页;
  • 支持上古级别的浏览器(IE8-)。

缺点:

  • 不太适合第三方API,给第二方使用较麻烦;
  • iframe对浏览器性能影响较大;
  • 没法使用非协议默认端口的API;
  • 须要特殊接口支持,不能基于REST的API规范。

window.name

这方法利用了window.name的特性:一旦被赋值后,当窗口被重定向到一个新的URL时不会改变它的值。这一行为使得不一样域的特定文档能够读取该属性值,所以能够绕过同源策略并使跨域消息通讯成为可能。

【PS】例子里演示的是发起get请求,只要把请求地址直接写到src里就好了。若是想要发起其余类型的请求,能够类比采用模拟的form的方式进行改造。

原理及流程

  1. 新建iframe,使用iframe访问一个非同源的地址(发请求);
  2. 当页面加载完成后,iframe内脚本给window.name属性赋值,这时父页面仍是不能读取到子页面的属性(由于不一样源);
  3. iframe自身回调到一个同源的地址(可能只是个空白页),这时候window.name没有改变;
  4. 父页面顺利读取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
<!DOCTYPE html>
<html>
    <script>
    function init(){
        window.name = 'hello';
        window.location = 'http://demo.com/empty.html'
    }
    </script>
    <body onload="init();"></body>
</html>

总结

优势:

  • 能够发送任意类型的请求;
  • 不须要设置子域名。

缺点:

  • iframe对浏览器性能影响较大;
  • 须要特殊接口支持,不能基于REST的API规范;
  • 每当你想要获取一条新的消息时都不得不发起两次网络请求,网络成本大;
  • 须要准备空白页,对它的访问是无心义的,影响流量统计。

window.hash

这个方法利用了location的特性:不一样域的页面,能够写不可读。而只改变哈希部分(井号后面)不会致使页面跳转。也就是可让父、子页面互相写对方的location的哈希部分,进行通信。

原理及流程

  1. 新建iframe,使用iframe访问一个非同源的地址(发请求),参数里带上父页面url;
  2. 当页面加载完成后,iframe内脚本设置父页面的url并在哈希部分带上数据;
  3. 父页面的脚本循环检查哈希值的变化,若是检查到有值就取值并清空哈希值;
【PS】父页面会循环检查哈希是否改变来读取值,由于这种降级方案的使用环境通常是不会有hashchange事件的。演示里是最简单的get方法,若是想要发起其余类型的请求,能够类比采用模拟的form的方式进行改造,但记住不要丢失父页面的url。

window.hash流程图

// 获取当前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
<!DOCTYPE html>
<html>
    <script>
    function init(){
        // 剪裁出父页面的url
        var parentUrl = '';
        var url = window.location.href;
        var str = url.split('?')[1].replace('?', '');
        strs = str.split("&");
        for(var i = 0; i < strs.length; i ++) {
            if(strs.split("=")[0] === 'url'){
                parentUrl = strs.split("=")[1];
            }
        }
        // 设置到父页面上
        window.parent.location = decodeURIComponent(parentUrl) + '#helloworld';
    }
    </script>
    <body onload="init();"></body>
</html>

总结

优势:

  • 能够发送任意类型的请求;
  • 不须要设置子域名。

缺点:

  • iframe对浏览器性能影响较大;
  • 须要特殊接口支持,不能基于REST的API规范;
  • 循环检查哈希须要消耗性能;
  • 返回数据受浏览器URL最大长度2083字符限制。

现代的标准

W3C的标准化跨域方案,让现代浏览器跨域已经不是什么复杂的事。这部分网上资料已经不少,这里就只是简单介绍。

CORS

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它容许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS参考文档
跨域资源共享 CORS 详解

postMessage

H5的window.postMessage为浏览器带来了一个安全的。基于事件的消息api。
只要是window对象,基本均可以使用这个方法,也就是说window.name、window.hash这类风骚的操做都已成为降级方案。

postMessage参考文档

安全问题

上述的各种非标准的骚操做,都算是对同源策略的破解办法,在方便开发者完成跨域目的的同时,各种恶意的攻击者也天然会利用这些方案为非做歹。 其中子域名代理的风险最低,由于须要服务器设置特定的子域名,也就是已是两个源的协商结果,通常黑客是难以模拟的。 风险最高的要算JSON-P的方案,由于这是任何客户端均可随意使用的办法,CSRF攻击的核心也是利用了特定标签的跨域性发起请求,因此JSON-P最好用在无用户状态的低安全性API上。

相关文章
相关标签/搜索