1. 同源策略
1.1 含义
1995年,同源策略由Netscape公司引入浏览器。目前所有浏览器都支持同源策略。所谓“同源”指的是协议、域名、端口三方面都相同,否则为不同源。举例说明: 针对网址 www.example.com/dir/page.ht… ,协议为http://;域名为www.example.com;端口为80(默认);与之同源的情况如下
1. http://www.example.com/dir2/other.html: 同源
2. http://example.com/dir/other.html:不同源(域名不同)
3. http://v2.www.example.com/dir/other.html:不同源(域名不同)
4. http://www.example.com:81/dir/other.html:不同源(端口不同)
1.2 目的
同源策略的目的是为了保护用户信息的安全,防止他们窃取该用户的信息。
1.3 限制范围
伴随着互联网以及技术的不断发展,同源策略的限制越来越严格,就目前来看限制的行为主要包括以下三种形式。
- 获取网页的存储信息,主要为Cookie、localStorage
- Dom元素的操作,主要为Iframe
- Ajax操作(可以发请求,但是服务端无响应)
下面,将详细介绍这三方面的限制,以及如何规避限制。
2. Cookie、localStorage的限制
Cookie只要是服务端发送给浏览器端的一段信息,这段信息受浏览器同源策略的限制,只有在同源的情况下,才可以共享访问。但是,如果两个网页的一级域名相同,只有二级域名不相同的情况下,我们可以在页面中通过设置相同的document.domain,可以实现两个页面的cookie共享。如下: A网页的的地址为 w1.example.com/a.html; B网页的地址为 w2.example.com/b.html ;那么只要设置相同的document.domain,两个网页就可以共享Cookie。如下:
document.domain = "example.com"
现在,在A网页的脚本中设置一个Cookie
document.cookie = "name = hello";
B网站就可以读到这个Cookie
let allCookie = document.cookie;
注意:这种方式只能适用于Cookie和iframe中,对于LocalStorage可以使用下面介绍PostMessage API。 除此之外,我们也可以在服务器端设置cookie的domain属性,将其设置为一级域名,如下:
Set-Cookie: key=value; domain=.example.com
在这样的情况下,二级域名和三级域名就不需要设置,同样可以得到cookie中的信息。
3. 获取DOM的限制(以Iframe为例)
如果两个页面不同源,是不能获取到对方的Dom的,典型的例子就是iframe以及用window.open打开的页面,无法与父窗口进行通信。 如果在父窗口中获取iframe窗口的数据如下:
document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
由于受到同源策略的限制,父窗口想要获得子窗口的DOM,因跨域导致报错。反之,子窗口获取父窗口的Dom也受到同源策略的影响,同样报错
window.parent.document
// 报错
如果两个窗口存在与Cookie相同的情况,一级域名相同,二级域名不同,可以通过设置document.domain来规避跨域,获取DOM。 如果是两个完全不同源的网站,目前有三种方案可以解决跨域通信
-
片段识别符(fragment identifier)
-
window.name
-
跨文档通信API(Cross-document messaging)
3.1 片段识别符
所谓的片段标识符指的是URL中“#”后面的部分,例如 example.com/x.html#frag… 中 #fragment为片段标识符,修改URL的片段标识符,页面不会刷新。我们可以通过修改片段标识符,并监听其改变来实现跨域通信。 在父窗口中写入子窗口的片段标识符
let src = `${originURL}#${data}`;
document.getElementById("myIframe").src = src;
子窗口通过监听hashChange事件得到通知
window.onhashchange = function(){
let message = window.location.hash;
// ...
}
同样的原理,子窗口同样可以改变父窗口的hash
parent.location.href= `${target}#${hashData}` ;
3.2 window.name
window对象有一个name属性,该属性有一个特性:在一个窗口(window)的生命周期内,窗口加载的所有页面共享一个window.name属性,每个页面对该属性都有读写权限。该属性持久的存在窗口载入过得所有页面中,并不会因为新页面的载入而进行重置。 例如,我们有一个a.html页面,代码如下
// a.html
<script>
window.name = "我是持久化的属性值!"
setTimeout(() =>{
window.location = "b.html"
},3000); // 3s后打开一个新的页面b.html
</script>
在b.html页面中
// b.html
<script>
console.log(window.name) // 我是持久化的属性值!
</script>
我们看到b.html页面获取到了window.name属性的值,以后加载的页面都可以获取到这个值,也可以对该值进行修改。但是,window.name的值只能是字符串,大小在2M左右,取决于浏览器。 但是,我们获取window.name的值都是在同源下,如何在跨域的情况下获取到window.name的值呢?比如我们有一个www.example.com/a.html,该页面需要获取一个不同域的页面www.cnblogs.com/data.html里面的数据。data.html页面代码如下
// data.html
<script>
window.name = "我是a.html页面需要的数据!"
</script>
那么在a.html页面中,我们怎么把data.html页面载入进来呢?显然我们不能直接在a.html页面中通过改变window.location来载入data.html页面,因为我们想要即使a.html页面不跳转也能得到data.html里的数据。答案就是在a.html页面中使用一个隐藏的iframe来充当一个中间人角色,由iframe去获取data.html的数据,然后a.html再去得到iframe获取到的数据。 充当中间人的iframe想要获取到data.html的通过window.name设置的数据,只需要把这个iframe的src设为www.cnblogs.com/data.html就行了。然后a.html想要得到iframe所获取到的数据,还必须把这个iframe的src设成跟a.html页面同一个域才行,不然根据前面讲的同源策略,a.html是不能访问到iframe里的window.name属性的。这就是整个跨域过程。
// a.html
<body>
<iframe id='proxy' src='www.cnblogs.com/data.html' style="display:none" onload="getData()"></iframe>
</body>
<script>
function getData(){ // iframe载入data.html页面后执行此函数
let iframe = document.getElementById("proxy");
iframe.onload = function(){ // 此时a.html已经与iframe页面同源了,可以访问
let data = iframe.contentWindow.name; // 获取iframe里面的window.name
console.log(data);
}
iframe.src = "b.html" ; // 这里的b.html为随便一个与a.html同源的页面,目的是为了 // a.html 能访问到iframe里面的东西,设置成about:blank也可以
}
</script>
3.3 window.postMessage
html5提供了一个全新的API,跨文档通信 API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。 例如,我们在 aaa.com 页面中嵌入一个 bbb.com 不同源的iframe页面,父页面向子页面发送消息如下:
// http://aaa.com
window.onload = function() {
var ifr = document.getElementById('ifr');
var targetOrigin = "http://bbb.com";
ifr.contentWindow.postMessage('hello world!', targetOrigin);
};
postMessage的用法如下:
- otherWindow.postMessage(message, targetOrigin); 其中otherWindow为指目标窗口,也就是给哪个window发消息,是 window.frames 属性的成员或者由 window.open 方法创建的窗口。 message为是要发送的消息,类型为 String、Object。 targetOrigin为限定消息接收范围,不限制请使用"*"
bbb.com 页面通过监听 message事件监听并接受消息。
<script>
var onmessage = function (event) {
var data = event.data; //插入的信息
var origin = event.origin; //消息来源地址
var source = event.source; //源Window对象
};
</script>
同理,子页面向父页面发送消息,再父页面也通过message监听函数进行监听。
4. ajax请求
受到同源策略的限制,Ajax请求只能发给同源的网址,否则会报错!以下介绍几种Ajax跨域请求的解决方案。
- 服务器代理
- jsonp
- cors(跨域资源共享)
4.1 服务器代理
服务器代理是最好理解的形式,我们架设一台与 浏览器同源的服务器,向该服务器发送请求,由服务器向其他不同源的服务器请求资源返回。服务器代理原理是,同源策略是浏览器的安全策略,服务器不受此限制,故,可以通过中转服务器进行跨域请求。
4.2 JSONP
JSONP是浏览器与服务器跨域通信常用的方式,它的优点是简单适用,兼容老实浏览器,服务器端改造比较小。由于script标签的src属性不受同源策略的影响,可以访问跨域的js脚本,JSONP就是基于这个原理实现跨域请求的。原理是通过在网页添加一个script元素,向服务器请求JSON数据,服务器接收到请求之后,将数据放在一个指定名字的回调函数里传回来。
第一步:网页动态插入元素,由它向跨域网址发送请求:
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=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
上面代码通过动态添加script元素,向服务器发送了请求。注意,这个请求中带了一个查询参数callback,用来指定回调函数的名字,这对于JSONP是必须的。 服务器接收到请求后,会将数据放在回调函数的参数位置返回。
foo({
"ip": "8.8.8.8"
});
由于script元素请求脚本直接作为代码运行。这是只要浏览器定义了foo函数,该函数就会立即调用。 注意:JSONP只支持GET请求,不支持其他请求方式。
4.3 CORS(跨域资源共享)
CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。支持各种求方式。 整个CORS通信的过程,都是浏览器自动完成的,不需要用户参与。对于前端开发者而言,CORS通信与同源AJAX通信一样,代码完全相同。浏览器一旦发现请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键点在于服务器端,只要服务器实现了CORS接口,就可以跨域通信。 如果想了解CORS请求的具体过程,请参看阮一峰老师的《跨域资源共享 CORS 详解》