4

前言

这是一道前端跨不过躲不掉面试必备的知识,挣扎多年没能做到刻骨铭心深入脊髓,只能好好写篇博文记录起来了;

什么是跨域?

广义来说,A域执行的文档脚本试图去请求B域下的资源是不被允许的,源于浏览器的同源策略,所谓同源指的是协议,域名 (domain) ,端口都相同,例如下面四个URL就属于不同源;
举例URL: http://www.a.com(:80)/index.html
协议不同: https://www.a.com(:80)/index.html
域名不同: http://www.b.com(:80)/index.html
端口不同: http://www.a.com(:8080)/index.html

为什么会有同源策略(Same origin policy)?

目的就是为了保证用户信息的安全,阻止和减少通过恶意分子窃取数据(例如普遍的CSRF攻击)。
首先同源策略也分两种XmlHttpRequest同源策略DOM同源策略,
我们常说的同源策略一般是指XmlHttpRequest同源策略;

例如用户登陆某个购物平台;

  1. 用户登陆www.shopping.com购物页,用户信息被保存在cookie中;
  2. 用户被引导进恶意诈骗页面www.spite.com,执行了当中的恶意代码请求www.shopping.com,默认会发送对应cookie信息;
  3. www.shopping.com验证通过,返回请求数据,这时候相当于账户信息和权限都在他人手中了

这一过程不为用户所知道,这是属于XmlHttpRequest同源策略,总的来说就是禁止使用XHR对象向不同源的服务器地址发起HTTP请求

至于DOM同源策略也是差不多道理

  • 用户被引导进恶意诈骗页面www.spite.com,它用iframe伪装成www.shopping.com购物页;
  • 用户登陆账号密码,恶意诈骗页面就能拿用户信息跨域访问www.shopping.com的DOM节点;

这一过程不为用户所知道,这是属于DOM同源策略,总的来说就是禁止对不同源页面DOM进行操作

于是乎同源策略应运而生,主要限制在于

  • Cookie、LocalStorage 和 IndexDB 无法读取。
  • DOM 无法获得。
  • AJAX 请求不能发送。

需要注意的是同源策略只对网页的HTML文档做了限制,对加载的其他静态资源如javascript、css、图片等仍然认为属于同源。

同源策略是绝对的么?

既然同源策略存在的重要性不言而喻,为什么我们需要尝试绕过这道坎?
例如银行的安全性够高了吧,相对应的限制性也是极高的,同理可得安全往往也是牺牲了部分灵活自由的。今时今日的互联网人流量,如果大型网站全部文件都只能访问指定一台服务器,根本不可能,也绝对支撑不起双十一的电商网站用户流量;于是在遵循同源策略的基础下,浏览器也适当地给开发者留下一些“密道”。

怎么绕过同源策略?

首先,一般来说协议和端口造成的跨域问题大部分方法是没有办法绕过的。

然后我们认识一下强大的跨域神器

IFrame 对象

IFrame 对象代表一个 HTML 的内联框架,由于它是独立的页面,因而拥有自己的事件,拥有自己的窗口对象(contentWindow);
图片描述
很多时候我们想要绕过同源策略都得经过iframe对象其他窗口对象

document.domain

这个方法实现前提:这两个域名必须属于同一个基础域名,即主域相同,子域可以不同;

原理

document.domain 默认的值是整个域名,所以即使两个域名的二级域名一样,那么他们的 document.domain 也不一样。通过把两个页面的document.domain都设置成相同域名(只能设置成自身或者更高一级的父域,且主域必须相同),两个页面之间可以获取window对象,和下图部分属性和方法;
图片描述

二级域名SLD(Second-level domain):是互联网DNS等级之中,处于顶级域名之下的域。二级域名是域名的倒数第二个部分,二级域名就是主域名分出来的域名。

  • 二级域名是寄存在主域名之下的域名。
  • 二级域名属于一个独立的分支,他有自己的收录、快照、PR值、反链等。
  • 当主域名受到惩罚,二级域名也会连带惩罚。

以此为例,我们有域名划分为:

.com 顶级域名
example.com 一级域名
A.example.com 二级域名
B.example.com 二级域名

example.com

<iframe src="http://A.example.com/" id="frame"></iframe>
<script type="text/javascript">
    document.domain = 'example.com';
    console.log(document.getElementById('iframe')); //得到iframe对象进行操作
</script>

A.example.com

document.domain = 'example.com';

一般使用不建议直接页面插入iframe,所以我们可以自己封装方法使用

function createIframe(url, id, callback) {
  var iframe = document.createElement('iframe');
  iframe.id = id || 'iframe';
  iframe.src = url;
  iframe.style.display = 'none';
  iframe.onload = function() {
    //this指向
    callback && callback.call(iframe);
  };
  document.body.appendChild(iframe);
}

也可以在服务器通过设置Set-Cookie,让客户端下拥有相同父域下所有子域名共享cookie;

window.name

原理

在一个窗口 (window) 的生命周期内,窗口载入的所有的页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。换成人话就是在同一个窗口打开的所有页面即使不同域名不同页面它们也会共享同一个name值(包括跳转,iframe等都属于并且可以支持非常长的 name 值2MB视浏览器而定)

优点

与 document.domain 方法相比,放宽了域名后缀要相同的限制,可以从任意页面获取 string 类型的数据。

例如下面会跳到百度,然后进入打印window.name依然能拿到设置的字符串

window.name = '123';
window.location.href = "http://www.baidu.com/";

上面的例子应该能起到一个抛砖引玉的作用了,接下来说所怎么通过window.name进行跨域;
条件,分别需要三个页面http://www.A.com/index.html,http://www.B.com/data.php,http://www.A.com/data.html,注意是否同源;

  • index.html通过动态创建一个iframe作为中间的桥梁,然后src属性指向远端服务器data.php发起请求;
  • data.php设置name,因为不能跨域操作name值,所以只能再次重定向回同源页面data.html;
  • 同源页面data.html可以直接获取数据,index.html也可以无障碍操作iframe,拿到name值后立马移除iframe保证安全;

http://www.A.com/index.html

function createIframe(url, id, callback) {
  var iframe = document.createElement('iframe');
  iframe.id = id || 'iframe';
  iframe.status = true;
  iframe.src = url;
  iframe.style.display = 'none';
  iframe.onload = function() {
    //this指向
    callback && callback.call(iframe);
  };
  document.body.appendChild(iframe);
}

createIframe('http://www.B.com/data.php', null, function() {
  //防止一直重定向
  if (this.status) {
    //跨域页面都没有读写权限,需要跳回同源页面操作
    iframe.status = false;
    this.contentWindow.location = 'http://www.A.com/data.html';
  } else {
    alert(JSON.parse(this.contentWindow.name));
    //释放内存,移除iframe
    this.contentWindow.document.write('');
    this.contentWindow.close();
    document.body.removeChild(this);
  }
});

http://www.B.com/data.html

<?php
  echo '<script> window.name = "{\"name\":\"data\"}"; </script>'
?>

location.hash

这个方法实现前提有两个:

  • 利用url改变hash(URL 的锚部分,从 # 号开始的部分)并不会导致页面刷新;
  • HTML 5新增的事件onhashchange,当#值发生变化时,就会触发这个事件;

优点:

  • HTTP请求过程中不会携带hash,所以这部分的修改不会产生HTTP请求,只有转码浏览器才会将其作为实义字符处理;
  • #后面出现的任何字符,都会被浏览器解读为位置标识符,浏览器只会滚动到相应位置,不会重新加载网页。

缺点:

  • 每次修改都会产生浏览器历史记录;
  • 有些浏览器不支持onhashchange事件,需要轮询来获知URL的改变;
  • 数据直接暴露在了url中
  • 过程繁琐,起码我认为是

思路:

条件,分别需要三个页面http://www.A.com/A.html,http://www.B.com/B.html,http://www.A.com/C.html,注意是否同源;

  1. A.html下创建iframe打开B.html,声明回调方法备用,并且修改hash;
  2. B.html下创建iframe打开C.html, 监听onhashchange事件改变hash;
  3. C.html下监听onhashchange事件,因为跟A.html同源可以往上层追踪触发到A.html的回调方法

http://www.A.com/A.html

function createIframe(url, id, callback) {
  var iframe = document.createElement('iframe');
  iframe.id = id || 'iframe';
  iframe.src = url;
  iframe.style.display = 'none';
  iframe.onload = function() {
    //this指向
    callback && callback.call(iframe);
  };
  document.body.appendChild(iframe);
}

createIframe('http://www.B.com/B.html', null, function() {
  var self = this;
  setTimeout(function() {
    self.src = self.src + '#name=123';
  }, 1000);
});

window.onCallback = function(res) {
  console.log('I got it!!', res);
};

http://www.B.com/B.html

function createIframe(url, id, callback) {
  var iframe = document.createElement('iframe');
  iframe.id = id || 'iframe';
  iframe.src = url;
  iframe.style.display = 'none';
  iframe.onload = function() {
    //this指向
    callback && callback.call(iframe);
  };
  document.body.appendChild(iframe);
}

createIframe('http://www.A.com/C.html');

var iframe = document.getElementById('iframe');
window.onhashchange = function() {
  iframe.src = iframe.src + location.hash;
};

http://www.A.com/C.html

window.onhashchange = function() {
  window.parent.parent.onCallback(
    'hello: ' + location.hash.replace('#name=', '')
  );
};

window.postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,可以安全地实现跨源通信。

优点:

  • 基本支持主流浏览器和IE8+,支持任意基本类型或可复制的对象;
  • 真正的跨域方法,包括协议和端口不同都可以实现;

缺点:

一般来说参数 message 被使用结构化克隆算法进行序列化。这意味着您可以将各种各样的数据对象安全地传递到目标窗口,而不必自己序列化它们,但部分浏览器只支持字符串,所以传参非字符串最好用JSON.stringify()序列化。
图片描述

先看看用法

otherWindow.postMessage(message, targetOrigin, [transfer]);
参数 描述
otherWindow 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames
message 将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化
targetOrigin 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
transfer(可选) 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权

http://www.A.com/index.html

function createIframe(url, id, callback) {
  var iframe = document.createElement('iframe');
  iframe.id = id || 'iframe';
  iframe.src = url;
  iframe.style.display = 'none';
  iframe.onload = function() {
    //this指向
    callback && callback.call(iframe);
  };
  document.body.appendChild(iframe);
}

createIframe('http://www.B.com/data.html', null, function() {
  var data = {
    name: '123',
  };
  this.contentWindow.postMessage(JSON.stringify(data), 'http://www.B.com');
});

window.addEventListener(
  'message',
  function(e) {
    console.log(e.data);
  },
  false
);

http://www.B.com/data.html

window.addEventListener(
  'message',
  function(e) {
    console.log(JSON.parse(e.data));
    window.parent.postMessage('I got it!', 'http://www.A.com/');
  },
  false
);

在这里出于本地调试原因,所以没有验证过信息,实际使用中务必使用originsource属性验证发件人的身份,targetOrigin设置可信任的网站。
有兴趣的可以在本地打开两个页面通信,targetOrigin设为*就可以了,然后得到下图的对象
图片描述

JSONP (JSON with padding)

这个方法实现前提有两个:

  • 还记得上面提过的同源策略只对网页的HTML文档做了限制,对加载的其他静态资源如javascript、css、图片等仍然认为属于同源。
  • JSON的诞生:

    • 指的是 JavaScript 对象表示法(JavaScript Object Notation)
    • 是轻量级的文本数据交换格式
    • 独立于语言:JSON 使用 Javascript语法来描述数据对象,但是 JSON 仍然独立于语言和平台。JSON 解析器和 JSON 库支持许多不同的编程语言。 目前非常多的动态(PHP,JSP,.NET)编程语言都支持JSON。
    • 具有自我描述性,更易理解

优点:

  • 因为script标签引入的文件内容是不能够被客户端的js获取到的(实际上凡是拥有"src"这个属性的标签都拥有跨域的能力),不会影响到被引用文件的安全,所以通过script标签引入的js是不受同源策略的限制的。而通过ajax加载的文件内容是能够被客户端js获取到的,所以ajax必须遵循同源策略,否则被引入文件的内容会泄漏或者存在其他风险;
  • 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;
  • JSON的纯字符数据格式可以简洁的描述复杂数据,易于处理这种格式的数据;
  • 不需要多余的中转操作;

缺点:

  • 一旦开始没法取消停止,没法重新开始,也没法捕捉错误;
  • JSONP是一种脚本注入(Script Injection)行为,所以有一定的安全隐患;
  • 只能用GET请求;

思路:

  1. 在客户端先声明回调函数例如callbackName,然后插入一个script标签指向请求地址同时传入回调函数名字进行跨域请求数据;
  2. 服务端接收到之后生成json数据,然后以入参的方式放置到一个函数名为callbackName的function,再把这段js文档返回给客户端;
  3. 浏览器解析script标签,并执行返回的javascript文档,此时数据作为参数,执行客户端预先声明好的回调函数;
//必须声明在请求之前
function callbacnName(res) {
  //doSometing
}

function createScript(url, callbackName) {
  var script = document.create('script');
  script.type = 'text/javascript';
  script.src = url + '?callback=' + callbackName;
  document.head.appendChild(script);
}

createScript('http://www.A.com/xxx.php', callbacnName);

整个过程就像前端发个信息给后台说麻烦返回一个这种格式的js代码给我,然后后台返回浏览器解析执行了这段代码

callbackName({
  xx: 'xxx',
});

在jQuery里用法如下

//实际请求的url地址会变成http://www.A.com/xxx.php?callback=callbackName
$.ajax({
  url: 'http://www.A.com/xxx.php',
  type: 'GET',
  dataType: 'jsonp',
  jsonp: 'callback', //传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(默认为:callback)
  jsonpCallback: 'callbackName', //自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名
  success: function(res) {
    //doSometing
  },
});

CORS(Cross-Origin Resource Sharing)跨域资源共享

原理:

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)

优点:

  • CORS支持所有类型的HTTP请求;
  • CORS使用普通的XMLHttpRequest发起请求和获得数据,可以捕捉到错误处理;
  • 所有浏览器都支持CORS,IE浏览器不得低于10(IE8/9需要使用XDomainRequest对象来支持CORS);

图片描述

因为浏览器基本支持,前端代码差别不大,主要实现细节都在浏览器和服务器,想要了解更多细节,请看
阮一峰老师的跨域资源共享 CORS 详解
网上参考的利用CORS实现跨域请求
很详细的文章HTTP访问控制(CORS)

因为第二篇文章的实例写的比我好多了,主要是做好兼容跟容错,我就放出他的代码好了,请允许我偷个懒

function createCORSRequest(method, url) {
  var xhr = new XMLHttpRequest();
  if ('withCredentials' in xhr) {
    // 针对Chrome/Safari/Firefox.
    xhr.open(method, url, true);
  } else if (typeof XDomainRequest != 'undefined') {
    // 针对IE
    xhr = new XDomainRequest();
    xhr.open(method, url);
  } else {
    // 不支持CORS
    xhr = null;
  }
  return xhr;
}

// 辅助函数,用于解析返回的内容
function getTitle(text) {
  return text.match('')[1];
}

// 发送CORS请求
function makeCorsRequest() {
  // bibliographica.org是支持CORS的
  var url = 'http://bibliographica.org/';

  var xhr = createCORSRequest('GET', url);
  if (!xhr) {
    alert('CORS not supported');
    return;
  }

  // 回应处理
  xhr.onload = function() {
    var text = xhr.responseText;
    var title = getTitle(text);
    alert('Response from CORS request to ' + url + ': ' + title);
  };

  xhr.onerror = function() {
    alert('Woops, there was an error making the request.');
  };

  xhr.send();
}

Afterward
621 声望62 粉丝

努力去做,对的坚持,静待结果