浏览器同源策略
含义
同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。同源 指的是“三个相同”:
- 协议相同
- 域名相同
- 端口相同
举例来说,http://www.example.com/index.html 这个网址,协议是
http://
,域名是www.example.com
,端口是80
(默认端口可以省略)。
目的
同源策略的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
限制范围
随着互联网的发展,"同源政策"越来越严格。目前,如果非同源,共有三种行为受到限制。
- Cookie、LocalStorage 和 IndexedDB 无法读取。
- DOM 无法获得。
- AJAX 请求不能发送。
虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,我将详细介绍,如何规避上面三种限制。
一、Cookie
1、浏览器设置
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';
注意:这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源策略,而要使用下文介绍的 PostMessage
API。
2、服务器设置
另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,如下:
Set-Cookie: key=value; domain=.example.com; path=/
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。
二、iframe、LocalStorage
如果两个网页不同源,就无法拿到对方的 DOM。典型的例子是 iframe
窗口和window.open
方法打开的窗口,它们与父窗口无法通信。
- 如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的
document.domain
属性,就可以规避同源政策,拿到DOM。 - 对于完全不同源的网站,目前有三种方法,可以解决 跨域窗口的通信问题:
1、片段识别符 #
片段标识符指的是,URL 的 # 号后面的部分。我们可以将信息写入到其他窗口的片段标识符,然后该窗口通过读取片段标识符来达到通信的目的。注:如果只是改变片段标识符,页面不会重新刷新。
父窗口写入
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
子窗口读取
window.onhashchange = function () {
var message = window.location.hash;
console.log(message);
}
子窗口改变父窗口的片段标识符
parent.location.href= target + "#" + hash;
2、window.name
无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页就可以读取;
子窗口写入
window.name = data;
父窗口读取
var data = document.getElementById('myFrame').contentWindow.name;
优点:容量很大,可以放置非常长的字符串;
缺点:必须监听子窗口属性的变化,影响网页性能。
3、跨文档通信API
前两种方法属于破解性质,并且使用有很大的局限性。HTML5 为了解决这个问题,引入了一个全新的API:跨文档通信 API。新增了 window.postMessage()
方法,允许跨窗口通信,不论这两个窗口是否同源。
语法
targetWindow.postMessage(message, targetOrigin,[ transfer ])
API | 描述 |
---|---|
targetWindow | 接收消息的窗口的引用。获得此类引用的方法包括:Window.open (生成一个新窗口然后引用它);window.opener (引用产生这个的窗口);HTMLIFrameElement.contentWindow (从其父窗口引用嵌入式);window.parent (从嵌入式内部引用父窗口);window.frames +索引值(命名或数字)。 |
message | 要发送到其他窗口的数据。使用结构化克隆算法序列化数据。这意味着您可以将各种各样的数据对象安全地传递到目标窗口,而无需自己序列化。 |
targetOrigin | 接收消息的窗口的源,可以是 URI ,也可以是 * ,表示不限制域名,向所有窗口发送。 |
transfer | 可选的。是与消息一起传输的 Transferable 对象序列。这些对象的所有权将提供给目标端,并且它们在发送端不再可用。 |
实例
例如:父窗口为 http://p.com ,子窗口为 http://c.com :
父向子发送信息
// 父文件
var popup = window.open('http://c.com', 'title');
popup.postMessage('How are you?', 'http://c.com');
子向父发送信息
// 子文件
window.opener.postMessage('I am fine, and you?', 'http://p.com');
message 事件
otherWindow 可以通过侦听 message
事件来监听已发送的消息:
window.addEventListener('message', function(event) {
console.log(event.data);
}, false);
属性 | 描述 |
---|---|
event.data | 传递的信息。 |
event.origin | 消息发向的网址。 |
event.source | 对发送消息的窗口的引用;你可以使用它来建立两个不同来源的窗口之间的双向通信。 |
消息过滤
// 父文件
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
if (event.origin !== 'http://p.com') return;
if (event.data === 'How are you?') {
event.source.postMessage('Hi', event.origin);
}
else {
console.log(event.data);
}
}
延伸(LocalStorage)
// 父窗口发送消息
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = {
name: 'Guang'
};
// 存入对象
win.postMessage(JSON.stringify({
key: 'storage',
method: 'set',
data: obj
}), 'http://c.com');
// 读取对象
win.postMessage(JSON.stringify({
key: 'storage',
method: "get"
}), "*");
window.onmessage = function (e) {
if (e.origin !== 'http://p.com') return;
console.log(JSON.parse(e.data).name); // "Guang"
};
// 子窗口接收消息
window.onmessage = function (e) {
if (e.origin !== 'http://c.com') return;
var res = JSON.parse(e.data);
switch (res.method) {
case 'set':
localStorage.setItem(res.key, JSON.stringify(res.data));
break;
case 'get':
var parent = window.parent;
var data = localStorage.getItem(res.key);
parent.postMessage(data, 'http://p.com');
break;
case 'remove':
localStorage.removeItem(res.key);
break;
}
};
三、AJAX 请求
说明:受同源策略的限制,AJAX 请求只能发给同源的网址,否则就报错。
解决:除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。
1、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 () {
// 指定请求地址 以及 回调函数的名称
addScriptTag('http://example.com/ip?callback=complete');
}
// 回调函数
function complete(data) {
console.log(data);
};
说明
- 动态添加
<script>
元素,向服务器example.com
发出请求; - 该请求的查询字符串有一个
callback
参数,用来指定回调函数的名字,这对于JSONP是必需的; - 作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse
优点:简单适用,老式浏览器全部支持,服务器改造非常小。
缺点:只能发送 GET
请求
2、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
:表示该请求的请求源,即发自哪个域名。服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
3、跨域资源共享(CORS)
介绍
CORS 是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。
特点
- CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
- 前端不需要做任何处理,对于开发者来说,CORS通信 与同源的 AJAX通信 没有差别,代码完全一样。
- 浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
- 实现CORS通信的关键是服务器。只要服务器实现了 CORS 接口(做相应配置),就可以跨源通信。
两种请求
浏览器将CORS请求分成两类:简单请求和非简单请求。只要同时满足以下两大条件,就属于简单请求:
-
请求方法是以下三种方法之一:
HEAD
GET
POST -
HTTP的头信息中 Content-Type 只限于三个值:
application/x-www-form-urlencoded
multipart/form-data
text/plain
凡是不同时满足上面两个条件,就属于非简单请求。而浏览器对这两种请求的处理,是不一样的。
3.1、CORS 简单请求
浏览器:对于简单请求,浏览器会自动在头信息(Request Headers
)之中,增加一个 Origin
字段。用来说明本次请求来自哪个源(协议 + 域名 + 端口):
Origin: http://api.cors.com
服务器:需要指定跨域请求的许可范围 Access-Control-Allow-Origin
,值可以是具体的源地址 或者是 *
表示所有范围都适用。
var express = require('express');
var app = express();
app.use();
app.all('*', function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
next();
});
-
如果 Origin 指定的源在许可范围内,服务器返回的响应(
Response Headers
),会多出几个头信息字段。Access-Control-Allow-Origin: http://api.cors.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar
-
如果 Origin 指定的源,不在许可范围内,服务器会返回的响应头信息不会包含
Access-Control-Allow-Origin
字段,从而抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
3.2、CORS 非简单请求
非简单请求一般对服务器有特殊要求,比如请求方法是 PUT
或 DELETE
,或者 Content-Type
字段的类型是 application/json
。
浏览器:对于非简单请求,浏览器会在正式通信之前,先发送一次HTTP查询请求,称为预检请求(
options请求)。如果服务器支持该请求,则会将真正的请求发送到后端,反之,如果浏览器发现服务端并不支持该请求,则会在控制台抛出错误。
服务器配置:
var express = require('express');
var app = express();
app.use();
app.all('*', function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
CORS字段介绍:
字段 | 描述 |
---|---|
Access-Control-Allow-Methods | 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次预检请求。 |
Access-Control-Allow-Headers | 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。 |
Access-Control-Allow-Credentials | 该字段与简单请求时的含义相同。 |
Access-Control-Max-Age | 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。 |