关于跨域简单总结
什么是浏览器同源策略?
同源是指,域名,协议,端口号均相同,如图:
注意:localhost和127.0.0.1虽然都指向本机,但也是跨域.
浏览器同源策略(same-origin policy).简单的讲,同源策略就是浏览器为了保证用户安全,防止恶意的网站盗取数据,禁止不同域之间的JS进行交互.会限制如下行为:
- Cookie,LocalStorage 和indexDB无法读取.
- DOM无法获取.
- AJAX请求不能发送.
解决跨域的方法有如下几种
CORS资源共享
CORS是跨域资源共享(Crocss-Origin Resource Sharing)的缩写.是W3C标准.跨域资源共享机制允许Web应用服务器进行跨域访问控制,从而使跨域传输得以安全进行.对于这个方式阮一峰老师有一篇文章专门讲这个的,跨域资源共享CORS详解可供参考.还有就是MDN里有关于HTTP访问控制的文档.HTTP访问控制CORS
简单的说就是:
请求方在头信息中多一个Origin字段来说明本次请求来自哪个源.(协议+域名+端口)
服务器响应头信息中会多几个字段
- Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求.
- Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可.
- Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
开发者需要在AJAX中设置withCredentials属性为true.
JSONP跨域
JSONP是JSON with Padding的简称.这是一种利用浏览器漏洞解决跨域的办法.HTML脚本元素可以不受浏览器同源策略的限制。即我们请求HTML、CSS、JS文件时浏览器不会做限制。所以运用这个特点,通过加载外部js文件的方式来向其他资源发出http请求。正是因为是请求文件,所以JOSNP只能GET,不能POST.简单来说,JSONP方案就是页面动态创建一个script标签.
<script src="www.baidu.com/data.js?callback=Callback">
然后服务端根据我们传过去的参数,进行处理.
举个例子:
callBackFunc = function(data){
console.log(data);
};
var script = document.createElenent('script');
script.type = "text/javascript";
script.src = "http://www.localhost:8888/data.json?callBack= callBackFunc";
document.body.appendChild(script);
JSONP的缺点:
- 只能进行GET请求,无法发送POST请求.
- 缺乏错误异常处理机制,如果没有调用成功,也没有任何提示.例如404无法检测具体错误原因.
- 严重安全漏洞,借助JSONP可进行跨站伪造攻击(CSRF).
服务器代理
浏览器有跨域限制,但是服务器没有,所以可以由服务器请求所要域的资源再返回给客户端。
服务器可解决一切跨域问题.
window.postMessage方案
window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
window.postMessage() 方法被调用时,会在所有页面脚本执行完毕之后(e.g., 在该方法之后设置的事件、之前设置的timeout 事件,etc.)向目标窗口派发一个 MessageEvent 消息。 该MessageEvent消息有四个属性需要注意: message 属性表示该message 的类型; data 属性为 window.postMessage 的第一个参数;origin 属性表示调用window.postMessage() 方法时调用页面的当前状态; source 属性记录调用 window.postMessage() 方法的窗口信息。
关于这个的文章可参考window.postMessage
举个例子:
页面和其他打开的新窗口的数据传递
//同域 - a页面
<a href="./b.html" target="_blank">跳转</a>
<script>
window.addEventListener('message', function(event) {
if(event.origin == 'http://hwc.home.com:3000') { //同域与不同域,都是靠origin判断
console.log(event.data)
}
})
</script>
//b页面
<script>
document.getElementById('app').onclick = function() {
var target = 'http://hwc.home.com:3000'
opener.postMessage('b.html send message', target)
}
</script>
页面与嵌套的iframe消息
//a页面
<iframe src="http://hwc.home.com:4000/home.html"></iframe>
var iframe = document.getElementsByTagName('iframe')[0];
window.onMessage = function(event) {
if(event.source == iframe.contentWindow) {} //嵌套iframe除了origin外还可以比较用source
}
//iframe页面
window.parent.postMessage(data, target)
window.addEventListener('message',function(e){
if(e.source!=window.parent) return; //子页面可以通过此方法判断是否是父页面的消息
window.parent.postMessage('data','*');
},false);
多窗口之间消息传递不是指我用浏览器分别打开了两个页面,就能互相通信了。需要一个otherWindow,一个其他窗口的引用 window.postMessage
语法 ——> otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow ——> 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
message ——> 将要发送到其他 window的数据
targetOrigin——> 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。
iframe解决跨域
iframe跨域也是利用浏览器中带有src的标签可以跨域访问.凡是拥有src属性的标签都有跨域的能力.例如:<script> ,<img> ,<iframe>.
1) document.domain + iframe
对于主域相同,子域不同的情况,可以通过设置document.domain的办法解决.具体的做法是可以在http://www.a.com/a.html
和http://script.a.com/b.html
两个文件中分别加上document.domain = ‘a.com’;然后通过a.html文件中创建一个iframe,去控制iframe的contentDocument,这样两个js文件之间就可以“交互”了。当然这种办法只能解决主域相同而二级域名不同的情况,如果你异想天开的把script.a.com的domian设为alibaba.com那显然是会报错地!
主域名:就是指一个人通过域名注册商注册的那个名字(domain name),它在注册商的系统中有个唯一的域名ID。例如www.baidu.com,主域名就是baidu.com.而主域名左边的就是他的子域名.
代码如下:
父窗口(http://www.a.com/a.html
)
//<iframe id="iframe" src="http://script.a.com/b.html"></iframe>
<script>
var iframe = document.createElement('iframe');
iframe.src = 'http://script.a.com/b.html';
iframe.style.display = 'none';
document.body.appendChild('iframe');
//设置domain值.
document.domain = 'a.com';
var user = 'admin';
</script>
子窗口(http://script.a.com/b.html
)
<script>
document.domain = 'a.com';
// 获取父窗口中变量
console.log('data from parent=> ' + window.parent.user);
</script>
这样就能在子窗口获取父窗口里的数据了.
1、安全性,当一个站点(b.a.com)被攻击后,另一个站点(c.a.com)会引起安全漏洞。
2、如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。
2) location.hash + iframe
对于主域不同的情形,可用location.hash 和window.name解决.
原理是利用location.hash来进行传值。在url: http://a.com#segmentfault
中的‘#segmentfault’就是location.hash,改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递,当然数据容量是有限的。假设域名a.com下的文件test1.html要和b.com域名下的test2.html传递信息,test1.html首先创建自动创建一个隐藏的iframe,iframe的src指向b.com域名下的test2.html页面,这时的hash值可以做参数传递用。test2.html响应请求后再将通过修改test1.html的hash值来传递数据(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe;Firefox可以修改)。同时在test1.html上加一个定时器,隔一段时间来判断location.hash的值有没有变化,一点有变化则获取获取hash值。
a.com下的test1.html
function startRequest(){
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'http://www.b.com/test2.html#paramdo';
document.body.appendChild(iframe);
}
function checkHash() {
try {
var data = location.hash ? location.hash.substring(1) : '';
if (console.log) {
console.log('Now the data is '+data);
}
} catch(e) {};
}
setInterval(checkHash, 2000);
b.com域名下的test2.html
//模拟一个简单的参数处理操作
switch(location.hash){
case '#paramdo':
callBack();
break;
case '#paramset':
//do something……
break;
}
function callBack(){
try {
parent.location.hash = 'somedata';
} catch (e) {
// ie、chrome的安全机制无法修改parent.location.hash,
// 所以要利用一个中间的b.com域下的代理iframe
var iframeproxy = document.createElement('iframe');
iframeproxy.style.display = 'none';
iframeproxy.src = 'http://a.com/test3.html#somedata';
// 注意该文件在"a.com"域下
document.body.appendChild(iframeproxy);
}
}
a.com域名下的test3.html
//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);
缺点,暴露数据在url中.
3) window.name + iframe
对于主域不同的情形,可用location.hash 和window.name解决.
iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
有三个页面:
- a.com/test1.html :应用页面
- a.com/proxy.html :代理页面,需要和应用页面在同一域下.
- b.com/test2.html : 存放应用页面需要用到数据的页面(数据页面)
具体过程:
1) 应用页面创建一个iframe,src指向数据页面.数据页面会把数据附加到这个iframe的window.name上.
b.com/test2.html 代码
<script type="text/javascript">
window.name = 'I was data!';
// 这里是要传输的数据,大小一般为2M,IE和firefox下可以大至32M左右
//数据格式可以自定义,如json、字符串
</script>
2) 在应用页面中监听iframe的onload事件,在事件中设置这个iframe的src指向本地域的代理文件(代理页面和应用页面在同一域下,可以相互通信.)
a.com/test1.html 代码
<script type="text/javascript">
var state = 0,
iframe = document.createElement('iframe'),
loadFunction = function() {
if (state === 1) {
var data = iframe.contentWindow.name; // 读取数据
console.log(data); //打印'I was data!'
} else if (state === 0) {
state = 1;
iframe.contentWindow.location = "http://a.com/proxy.html"; // 设置的代理文件
}
};
iframe.src = 'http://b.com/test2.html';
if (iframe.attachEvent) {
iframe.attachEvent('onload', loadFunction);
} else {
iframe.onload = loadFunction;
}
document.body.appendChild(iframe);
</script>
3) 获取数据以后销毁这个iframe,释放内存.这也保证了安全性.(不被其他域iframe 访问)
<script type="text/javascript">
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
</script>
window.top方案
window.top方法可以访问最顶层的window对象,可以取到最顶层window对象的属性和方法。这样子框架就可以操作父页面的交互了。window.parent可以得到父框架的window对象。
1) a页面代码
<script type="text/javascript">
function funA(){
console.log("a页面的方法");
}
iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'http://a.com:8080/b.html';
document.body.appendChild(iframe);
</script>
2) b页面的代码
<script type="text/javascript">
console.log(window.top.funA());
function funB(){
console.log("b页面的方法");
}
iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'http://a.com:8080/c.html';
document.body.appendChild(iframe);
</script>
3) c页面代码
<script type="text/javascript">
console.log(window.parent.funB());
</script>
cookie 和 WebSocket 协议跨域
cookie除了有exprise这个属性之后还有path这个属性,这个属性是用来设置可访问到cookie的路径的,默认的是在当前cookie页面的子目录下是可以访问到的,但是默认的情况下我们是无法再其他父目录下面访问到这个cookie,这个时候我们就可以通过设置cookie来实现这个功能
如上面所说,但是在跨域之间的传值要怎样实现呢,其实cookie除了有path,还存在domain,domain这个属性是可以实现跨域的,但是必须保证这两个域名有公共部分,何为公共部分?就是像www.qq.com和www.sport.qq.com 这样都有相同的qq.com的域名,具体的使用方法是把domain设置为相同部分的域名,具体如下:
document.cookie = "name=value;expires=date;path=/;domain=qq.com"
另外在使用cookie的时候我们还需要注意的是cookie的编码问题,因为在cookie是不支持逗号、空格、分号的,所以在设置cookie的时候,需要使用escape()将输入的信息进行转码,然后在要调用的时候使用unescape()来重新转换回来.
WebSocket对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
WebSocket构造器方法接受一个必须的参数和一个可选的参数:
WebSocket WebSocket(in DOMString url, in optional DOMString protocols);
WebSocket WebSocket(in DOMString url,in optional DOMString[] protocols);
url :表示要连接的URL。这个URL应该为响应WebSocket的地址。
protocoles 可以是一个单个的协议名字字符串或者包含多个协议名字字符串的数组。这些字符串用来表示子协议,这样做可以让一个服务器实现多种WebSocket子协议(例如你可能希望通过制定不同的协议来处理不同类型的交互)。如果没有制定这个参数,它会默认设为一个空字符串。(可选)
构造器方法可能抛出以下异常:
SECURITY_ERR 试图连接的端口被屏蔽。
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。具体可参阅前端常见跨域解决方案
参考文档
H5新接口window.postMessage MDN
HTTP访问控制CORS MDN
跨域资源共享CORS详解
JavaScript跨域总结与解决办法
详解跨域(最全的解决方案)
浅谈几种跨域的方法
H5新接口webSocket MDN