浏览器安全的基石是”同源政策”(same-origin policy).

概述

所谓”同源”, 是指三个”相同”.

  • 协议相同: http:\\
  • 域名相同: www.example.comexample.com 不同
  • 端口相同

目的

同源政策的目的是为了保证用户信息安全, 防止恶意的网络窃取数据

限制范围

如果非同源, 共有三种行为受到限制

  • Cookie, LocalStorage 和 IndexDB 无法读取
  • DOM 无法获得
  • AJAX 请求无法发送

Cookie 是服务器写入浏览器的一小段信息, 只有同源的网站才能共享. 但是两个网页一级域名相同, 只有二级域名不同, 浏览器允许通过设置 document.domain 共享 Cookie.

举例来说, A 网页是 http://w1.example.com/a.html, B 网页是 http://w2.example.com/b.html, 那么只要设置相同的 document.domain, 这两个网页可以共享 Cookie.

document.domain = 'example.com';
`</pre>

现在 A 网页通过脚本设置一个 Cookie

<pre>`document.cookie = 'test1=hello';
`</pre>

B 网页就可以读取这个 Cookie

<pre>`var allCookie = document.cookie;
`</pre>

注意, 这种方法只适用于 Cookie 和 iframe 窗口, LocalStorage 和 IndexDB 无法通过这种方法, 规避同源政策.

另外, 服务器也可以在设置 Cookie 的时候指定 Cookie 所属域名为一级域名, 比如 `.example.com`

<pre>`Set-Cookie: key=value; domain=.example.com; path=/
`</pre>

这样的话, 二级域名和三级域名不用做任何设置就可以读取这个 Cookie

### iframe

如果两个网页不同源, 就无法拿到对方的 DOM, 典型的例子就是`iframe`窗口和`window.open` 方法打开的窗口, 他们与父窗口无法通信.

如果两个窗口一级域名相同, 只是二级域名不同, 那么设置`document.domain`也可以规避同源政策, 实现 DOM 获取.

### 完全不同源的网站

#### 片段识别符(fragment identifier)

片段识别符值得是 URL 中`#` 后的部分, 也就是 hash.

如果只是改变片段识别符, 页面不会重新刷新

父窗口可以把信息写入子窗口的片段识别符

<pre>`var src = originURL + '#' + data;
document.getElementById('myFrame').src = src;
`</pre>

子窗口通过监听 `hashchange` 事件得到通知

<pre>`window.onhashchange = checkMessage;

function checkMessage(){
  var message = window.location.hash;
  // ...
}
`</pre>

同样子窗口也可以改变父窗口的片段识别符

<pre>`parent.location.href = target + "#" + hash;
`</pre>

#### window.name

浏览器窗口有 `window.name` 属性, 这个属性的最大特点是, 无论是否同源, 只要在同一个窗口里, 前一个网页设置这个属性后, 后一个网页可以读取他

父窗口先打开一个子窗口, 载入一个不同源的网站, 该网页将写入 `widnow.name` 属性

<pre>`window.name = data;
`</pre>

接着, 子窗口跳回一个与主窗口同域的网址

<pre>`location = 'http:parent.url.com/xxx.html'
`</pre>

然后主窗口就可以读取子窗口的 `window.name` 属性了

<pre>`var data = document.getElementById('myFrame').contentWindow.name;
`</pre>

这种方法的优点是 `window.name` 容量大, 缺点是必须监听子窗口的 `window.name` 属性的变化, 影响性能.

#### window.postMessage

以上信息都属于破解, HTML5 为了解决这个问题, 引入一个全新的 API: 跨文档通信 API(Cross-document messaging)

这个 API 为 `window` 对象新增一个 `window.postMessage` 方法, 允许跨窗口通信, 不论两个窗口是否同源.

举例来说, 父窗口 `http://aaa.com` 向子窗口 `http://bbb.com` 发消息, 调用 `postMessage` 方法就可以了

<pre>`var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World', 'http://bbb.com');
`</pre>

`postMessage` 方法的第一个参数是要传递的 Message, 第二个参数是接受信息的窗口的源(origin), 即`协议+域名+端口`, 也可以设置为`*`, 表示不限制域名, 向所有窗口发送.

子窗口向父窗口发送消息的写法类似

<pre>`window.opener.postMessage('Hello World', 'http://aaa.com');
`</pre>

通过`message`事件监听对象消息

<pre>`window.addEventListener('message', function(e){
console.log(e.data);
}, false);
`</pre>

`message` 事件的事件对象 `event` , 提供一下三个属性
  • event.source: 发送信息的窗口, 子窗口通过 event.source 引用父窗口然后发送消息

    `window.addEventListener('message', receiveMessage);
    function receiveMessage(event){
    event.source.postMessage('Hello Opener', '*');
    }
    `
  • ‘event.origin’: 发送信息的网址(‘http://aaa.com'), 可以过滤不是指定来源的信息

    `window.addEventListener('message', receiveMessage);
    function receiveMessage(event){
    if (event.origin != 'http://aaa.com') return;
    if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);
    } else {
      console.log(event.data);
    }
    }
    `
  • ‘event.data’: 信息内容

    LocalStorage

    通过 `window.postMessage` 读写其他窗口的 `LocalStorage` 也是可能的

    例: 主窗口写入 iframe 子窗口的 localStorage

    `window.onmessage = function(event){
      if(event.origin !== 'http://bbb.com') return;
      var payload = JSON.parse(event.data);
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
    };
    `

    上面代码中, 子窗口将父窗口发送的消息写入自己的 localStorage

    父窗口发送消息的代码如下:

    `var win = document.getElementByTagName('iframe')[0].contentWindow;
    var obj = { name: 'Jack' };
    win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com');
    `

    加强版子窗口接收消息的代码

    `window.onmessage = function(e) {
      if (e.origin !== 'http://bbb.com') return;
      var payload = JSON.parse(e.data);
      switch(payload.method) {
        case 'set': localStorage.setItem(payload.key, JSON.stringify(parload.data));
                    break;
        case 'get': var parent = window.parent;
                    var data = localStorage.getItem(payload.key);
                    parent.postMessage(data, 'http://aaa.com');
                    break;
        case 'remove': localStorage.removeItem(payload.key);
                       break;
      }
    };
    `

    加强版的父窗口消息发送代码

    `var win = document.getElementByTagName('iframe')[0].contentWindow;
    var obj = { name: 'Jack' };
    // 存入对象
    win.postMessage(JSON.stringify(key: 'storage', method: 'set', data: obj), 'http://bbb.com');
    // 读取对象
    win.postMessage(JSON.stringify(key: 'storage', method: 'get',), '*');
    window.onmessage = function(e){
      if(e.origin !== 'http://bbb.com') return;
      // 'Jack'
      console.log(JSON.parse(e.data).name);
    };
    `

    AJAX

    同源政策规定, AJAX 请求只能发给同源的网址, 否则会报错

    除了假设服务器代理, 有三中方法规避这个限制

    JSONP

    JSONP 是服务器与客户端跨源通信的常用方法, 最大特点是简单实用, 老式浏览器全部支持, 服务器改造非常小.

    基本思想是, 网页通过添加一个&lt;script&gt;元素, 向服务器请求 JSON 数据,在何种做法不受同源政策限制. 服务器接收请求后, 将数据放在一个指定名字的回调函数里传回来.

    首先, 网页动态插入&lt;script&gt;元素, 由它向跨源网址发出请求.

    `function addScriptTag(src) {
      var script = document.createElement('script');
      script.setAttribute('type', 'text/javascript');
      script.src = src;
      document.body.appendChild(script);
    }
    window.onload = function(){
      addScritpTag('http://example.com/ip?callback=foo');
    }
    
    function foo(data){
      console.log("Your public IP address is: " + data.ip);
    };
    `

    上面的代码通过动态添加&lt;script&gt;元素, 向服务器example.com发出请求. 注意, 该请求的查询字符串有一个callback 参数, 用来指定回调函数的名字, 这对 JSONP 是必须的.

    服务器接收到这个请求后, 会将数据放在回调函数的参数位置返回.

    `foo({
      'ip': '8.8.8.8'
    });
    `

    由于&lt;script&gt;元素请求的脚本, 直接作为代码运行, 这时只要浏览器定义了foo函数, 该函数就会立即调用. 作为参数的 JSON 数据会被视为 JavaScript 对象, 而不是字符串, 因此避免了使用JSON.parse的步骤

    WebSocket

    WebSocket 是一种通信协议, 使用ws://(非加密)和wss://(加密)作为协议前缀.

    该协议不实行同源政策, 只要服务器支持, 就可以与他进行夸源通信.

    下面是一个例子, 浏览器发出的 WebSocket 请求的头信息:

    `GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    Origin: http://example.com
    
    

上面代码中, 有一个字段是Origin, 表示该请求的请求源(origin)

正式因为有了Origin这个字段, 所以 WebSocket 才没有实行同源政策, 因为服务器可以根据这个字段判断是否许可此次通信

CORS

CORS 是跨源资源分享(Corss-Origin Resource Sharing) 的缩写, 是 W3C 标准, 是跨源 AJAX 请求的根本解决办法. 相比 JSONP 只能发GET请求, CORS 允许任何类型的请求.