浏览器安全的基石是”同源政策”(same-origin policy).
概述
所谓”同源”, 是指三个”相同”.
- 协议相同:
http:\\
- 域名相同:
www.example.com
与example.com
不同 - 端口相同
目的
同源政策的目的是为了保证用户信息安全, 防止恶意的网络窃取数据
限制范围
如果非同源, 共有三种行为受到限制
- Cookie, LocalStorage 和 IndexDB 无法读取
- DOM 无法获得
- AJAX 请求无法发送
Cookie
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 是服务器与客户端跨源通信的常用方法, 最大特点是简单实用, 老式浏览器全部支持, 服务器改造非常小.
基本思想是, 网页通过添加一个
<script>
元素, 向服务器请求 JSON 数据,在何种做法不受同源政策限制. 服务器接收请求后, 将数据放在一个指定名字的回调函数里传回来.首先, 网页动态插入
<script>
元素, 由它向跨源网址发出请求.`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); }; `
上面的代码通过动态添加
<script>
元素, 向服务器example.com
发出请求. 注意, 该请求的查询字符串有一个callback
参数, 用来指定回调函数的名字, 这对 JSONP 是必须的.服务器接收到这个请求后, 会将数据放在回调函数的参数位置返回.
`foo({ 'ip': '8.8.8.8' }); `
由于
<script>
元素请求的脚本, 直接作为代码运行, 这时只要浏览器定义了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 允许任何类型的请求.