前言
同源政策:起源被定义为URI方案,主机名和端口号的组合。此策略可防止一个页面上的恶意脚本通过该页面的文档对象模型访问另一个网页上的敏感数据。
由于服务器基于HTTP cookie信息来揭示敏感信息或采取改变状态的行为,因此这种机制对于广泛依赖于HTTP cookie 的现代Web应用程序来维护经过验证的用户会话具有特殊意义。(维基百科)
(一)慨念
所谓的同源指的是“三个相同”
- (1)协议相同
- (2)域名相同
- (3)端口相同
举个例子来说
(二)安全应用程序
同源策略的概念可以追溯到1995年的Netscape Navigator 2。该策略最初设计用于保护对文档对象模型的访问,但后来扩展为保护全局JavaScript对象的敏感部分。
同源策略有助于保护使用经过验证会话的网站。
假设一种情况:A网站是一家银行,用户登陆以后,又去浏览其他网站,该网站在后台运行一些恶意JavaScript代码,该代码从银行网站请求数据。由于用户仍然登录到银行网站,因此恶意代码可以做任何用户在银行网站上可以执行的操作。
(三) 限制范围
随着互联网的发展,”同源政策”越来越严格。目前,如果非同源,共有三种行为受到限制。
(1) Cookie、LocalStorage 和 IndexDB 无法读取。
(2) DOM 无法获得。
(3) AJAX 请求不能发送。
(四)放宽同源政策
在某些情况下,同源策略的限制性过大,会对使用多个子域的大型网站造成问题。
目前有以下几种解决方案
- document.domain属性
- 跨源资源共享
- 跨文档消息传递
- JSONP
- WebSockets
(1)domain属性(iframe)
举例来说,A网页是http://v1.example.com/a.html,B网页是http://v2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie,以及iframe窗口和window.open方法打开的窗口,而LocalStorage 和 IndexDB 无法通过这种方法。
(2)pjax
单页SPA的实现原理目前有两种方式:
- window(onhashchange)
- window(onpopstate)
他们都可以通过改变片段标识符,页面不会重新刷新。
首先我们来看hash更改
举个例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>first</title>
</head>
<body>
<button>按钮1</button>
<button>按钮2</button>
<script>
var btn1 = document.body.children[0];
btn1.onclick = function () {
location.hash = 'first';
}
var btn2 = document.body.children[1];
btn2.onclick = function () {
location.hash = 'second';
}
window.onhashchange = function (e) {
console.log(location.hash);
}
</script>
</body>
</html>
上面代码分别给button绑定事件,然后更改hash,用window.onhashchange 监听hash更改,运行效果如下。
首先我们来看history(ie11+)
history可以记录一个数据状态
举个例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>history</title>
</head>
<body>
<button>按钮1</button>
<button>按钮2</button>
<script>
var btn1 = document.body.children[0];
btn1.onclick = function () {
history.pushState('first', 'haha1');
}
var btn2 = document.body.children[1];
btn2.onclick = function () {
history.pushState('second', 'haha2');
}
window.onpopstate = function (e) {
console.log(e.state);
}
</script>
</body>
</html>
记录历史状态。
运行效果如下
(3)window.name(iframe)
浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。
父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入window.name属性。
window.name = 'xxx';
接着,子窗口跳回一个与主窗口同域的网址。
location = 'http://test.url.com/xxx.html';
然后,主窗口就可以读取子窗口的window.name了。
var data = document.getElementById('testFrame').contentWindow.name;
这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。
(4)window.postMessage(iframe)
HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。既允许跨窗口通信,不论这两个窗口是否同源。
举例来说。父窗口http://father.com向子窗口http://son.com发消息,调用postMessage方法就可以了。
var popup = window.open('http://son.com', 'title');
popup.postMessage('hello son!', 'http://son.com');
上面postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即”协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。
子窗口向父窗口发送消息的写法类似。
window.opener.postMessage('Nice to meet you', 'http://father.com');
父窗口和子窗口都可以通过message事件,监听对方的消息。
window.addEventListener('message', function(e) {
console.log(e.source); // 发送消息的窗口
console.log(e.origin); // 消息发向的网址
console.log(e.data); // 消息内容
},false);
当然,这种方式。就可以通过窗口发来的消息,写入自己的LocalStorage。
(五)AJAX
同源政策规定,AJAX请求只能发给同源的网址,否则就报错。
(1)JSONP
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
它的基本思想是,网页通过添加一个
// 客户端
function addScript(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScript('http://test.com/usr?callback=foo');
}
function fo(data) {
console.log('callback message is: ' + data.message);
};
// 服务器必须声明回调
fo({
"message": "hello world"
});
由于script元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse的步骤。
(2)WebSocket
WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
浏览器发出的WebSocket请求的头信息中有个Origin字段,表示该请求的请求源(origin),即发自哪个域名。
正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。
(3)CORS
CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。
一个完整的CORS nodejs express中间件实现代码如下
module.exports = function (req,resp,next) {
resp.set('Access-Control-Allow-Origin','http://localhost:8080');
resp.set('Access-Control-Allow-Methods','GET,POST,PUT,DELETE');
resp.set('Access-Control-Allow-Headers','Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With');
next();
};