@author YogaZheng
鉴于本人目前正处于前端基础知识学习阶段,本文仅是在阅读了网络上诸多相关文章后的一个总结,加上一点自己写的小例子,更多的是用于自己的个人积累。看到这篇文章的朋友,我更建议大家去阅读本文参考资料中的文章(均附有链接),可以对跨域问题有更深刻的理解。
目录
同源与跨域
同源
同源策略
跨域
Cookie跨子域共享
iframe跨域窗口通信
document.domain
window.name(完整例子见附一)
location.hash
window.postMessage
AJAX跨域实现
JSONP
CORS
参考资料
附一:document.name在跨域中的应用举例
如果两个页面协议相同、域名相同、端口相同,则称这两个页面同源。相反的,如果两个页面的协议、域名、端口中有一个或多个不同,则称这两个页面非同源。
对于网址http://www.cityworks.cn:80而言,http://是协议,www.cityworks.cn是域名,80是端口(默认端口通常省略)。对于这个网址,其同源情况如下:
http://www,cityworks,cn/pages/newsInfo,html 同源,协议、域名、端口均相同
https://www.cityworks.cn:80 不同源,协议不同
http://www.yuque.com 不同源,域名不同
http://www.cityworks.cn:2333 不同源,端口不同
简单来说,同源策略是限制了两个源之间资源交互的一种浏览器安全机制。对于非同源网页/网站,会受到三种行为限制:
无法读取对方的Cookies、LocalStorage、IndexDB
无法获得对方的DOM
无法向对方发送AJAX请求
同源策略的制定是出于网络安全性考虑,防止恶意窃取数据,隔离潜在恶意文件。设想以下两种情景:
你打开了银行账户页面,然后又"不小心"打开了一个恶意网站,如果没有同源策略,此时该恶意网站就可以通过javascript脚本"随心所欲地"访问、窃取、修改你的银行信息,包括账号密码。
当你使用Cookie来维护用户的登录状态时(这也是我们现在经常做的),如果没有同源策略,这些Cookie信息就会泄露,其他网站就可以冒充这个登录用户。
如果两个页面非同源,我们试图进行这两个页面通信的行为称为跨域。
这里值得一提的是,跨域并非是浏览器限制了发起跨域请求,而是跨站请求可以正常发起,但返回结果被浏览器拦截了。也有特例,比如Chrome和Firefox浏览器对于从HTTPS协议访问HTTP协议的跨域请求在其未发出时就拦截。
在实际的项目开发过程中,我们总会不可避免的要进行跨域请求操作。但是,对于协议和端口不同的跨域问题,前端是无法解决的,需要通过后台实现。 所以通常而言,前端所说的跨域处理指的是对于不同域名通信的跨域实现,也就是本文讨论的主要内容。
我们知道,由于同源策略的限制,Cookie只有同源的页面才能共享。但是,如果两个页面主域名相同,子域名不同,浏览器允许通过设置docuement.domain共享Cookie和DOM。
举个栗子,A页面地址是http://a.example.com/a.html,B页面地址是http://b.example.com/b.html,那么只要两个页面将各自的document.domain指向同一主域example.com,它们就可以共享Cookie。
在A页面设置document.domain,并通过脚本设置一个Cookie:
document.domain = 'example.com';
document.cookie = 'favourite_food=chocolate';
在B页面设置相同的document.domain,就可以读取到这个Cookie:
document.domain = 'example.com';
console.log(document.cookie); //包括'favourite_food=chocolate'
另外,服务器也可以在设置Cookies的时候,指定Cookies的所属域名为主域名:
Set-Cookie: key=value; domain=.example.com; path=/
如此,二级、三级域名不用做任何设置,就都可以读取这个Cookie。
项目中会有使用iframe把其他域名的内容嵌入页面中的场景(比如登录/注册等表单提交浮窗),有时候会需要与父窗口进行通信。
如果打开窗口的主域与父窗口主域相同,子域名不同,那么同Cookie一样,可以使用document.domain进行通信,获取彼此的DOM。
举个栗子:在A页面http://a.example.com/a.html中有一个
父窗口a.html:
- 陈情
- 避尘
子窗口b.html:
- 魏无羡
- 蓝忘机
window.name有一个特征:在一个窗口(window)的生命周期内,窗口载入的所有页面都是共享一个window.name的,且每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的。也就是说,无论是否同源,只要在同一个窗口中,前一个页面设置了这个属性,后一个页面可以读取。
基于这个特征,我们可以实现两个域名完全不同的页面之间的通信,只要它们在同一个窗口内先后打开。
举个栗子,我们新建标签页,在地址栏输入https://www.baidu.com,在控制台输入:
window.name = 'Superman';
然后在地址栏输入http://www.cityworks.cn,在控制台查看window.name:
>> window.name
<< "Superman"
由此我们可以实现跨域页面的通信。
结合iframe,父窗口可以获取到子窗口下的window.name。
首先,父窗口http://parent.url.com/a.html载入了不同源的子窗口http://child.path.com/index.html:
接着,使子窗口跳回一个与主窗口同域的网页http://parent.url.com/b.html,此时该窗口的window.name不变:
document.getElementById('iframe').src = "http://parent.url.com/b.html;
如此,父窗口就可以通过读取此时子窗口的window.name,获取非同源页面的window.name了:
console.log(document.getElementById('iframe').contentWindow.name);
另外,结合iframe或window.open,window.name可以实现父窗口向子窗口传递数据。
在iframe中,只需指定标签的name属性即可:
然后根据上述父窗口获取子窗口的window.name的方式,我们可以发现,此时子窗口的window.name为"Thor"。
在window.open中,只需指定target即可:
window.open('http://child.path.com', 'Loki');
此时在打开窗口的控制台可以看到其window.name:
>> window.name
<< "Loki"
window.name在跨域使用上需要注意:
1.window.name仅支持string类型的数据,其他数据类型都会被强制转换为string。
>> window.name = 123
<< "123"
>> window.name = ['iron-man','captain-america']
<< "irom-man,captain-america"
>> window.name = {name: 'black-widow'}
<< "[object Object]"
>> window.name = null
<< "null"
>> window.name = undefined
<< "undefined"
2.window.name传递数据大小限制一般为2M,不同浏览器有一定差异。
对于网址http://example.com/index.html#ant-man,我们称该url的#号后面部分为片段标识符,片段标识符不会被发送到服务器端,不会引起页面刷新。利用片段标识符,我们可以把传递的数据依附在url上,实现非同源父窗口与子窗口之间的通信。显然,用片段标识符传递数据的方法既适用于iframe标签,也适用于window.open打开窗口传递数据。
举个栗子,父窗口向
子窗口可以通过监听事件检测片段标识符的变化:
window.onhashchange = checkData;
function checkData() {
var data = location.hash;
console.log(data); // "#ant-man"
}
同window.name类似,子窗口要通过location.hash向父窗口传递数据,需要在子窗口中再嵌入与父窗口同源的第三个窗口,将信息设置在第三个窗口的hash值上,然后第三个窗口改变父窗口的hash值,从而实现跨域。通过location.hash实现子窗口向父窗口传递数据的方法比较复杂,通常不做考虑。
location.hash在跨域使用上需要注意:
1.同window.name一样,location.hash仅支持string类型的数据。
2.location.hash字段是加在URL后的,因此受到URL长度限制,不同浏览器限制不同,如IE浏览器限制最长URL为2083个字符,Google Chrome限制为8182个字符。
HTML5为解决跨域通信的问题,引进了一个全新的API:跨文档通信API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,无论两个窗口是否同源。
otherWindow.postMessage(message, targetOrigin)
otherWindow:
其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
message:
将要发送到其他Window的数据,支持类型String、Object。
targetOrigin:
用于指定哪些窗口能接收到数据,其值可以是字符串"*"(表示任意窗口)或一个URI。
举个栗子,父窗口http://parent.url.com向子窗口http://child.path.com发送数据,使用postMessage方法:
var child = window.open('http://child.path.com');
child.postMessage('Wonder Woman', '*');
相反的,子窗口向父窗口发送数据:
window.opener.postMessage('Wolverine', '*');
进一步的,父窗口和子窗口都可以通过message事件,监听对方的消息。
window.addEventListener('message', function(event) {
console.log(event);
});
message事件的事件对象event提供三个属性:
event.source:发送消息的源窗口
event.origin:消息发送指向的网址
event.data:消息内容
举个栗子,子窗口可以通过event.source属性引用父窗口,从而使用postMessage向父窗口发送信息:
window.addEventListener('message', function(e) {
event.source.postMessage('Deadpool')
}
AJAX由于同源策略的限制,只能向同源的网址发送请求。为了突破这个限制,除了假设服务器代理(浏览器请求同源服务器,再由后者请求外部服务)以外,还可以通过JSONP和CORS方法向非同源服务器发送请求。
我们知道,凡是拥有src属性的标签(如