一、什么是跨域
跨域,指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器施加的安全限制。
1.1造成跨域的两种策略
浏览器的同源策略会导致跨域,这里同源策略又分为以下两种
(1)DOM同源策略:禁止对不同源页面DOM进行操作。这里主要场景是iframe跨域的情况,不同域名的iframe是限制互相访问的。
(2)XmlHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。
只要协议、域名、端口有任何一个不同,都被当作是不同的域,之间的请求就是跨域操作。
所谓同源是指,域名,协议,端口均相同,不明白没关系,举个栗子:
http://www.123.com/index.html 调用 http://www.123.com/server.php (非跨域)
http://www.123.com/index.html 调用 http://www.456.com/server.php (主域名不同:123/456,跨域)
http://abc.123.com/index.html 调用 http://def.123.com/server.php (子域名不同:abc/def,跨域)
http://www.123.com:8080/index.html 调用 http://www.123.com:8081/server.php (端口不同:8080/8081,跨域)
http://www.123.com/index.html 调用 https://www.123.com/server.php (协议不同:http/https,跨域)
请注意:localhost和127.0.0.1虽然都指向本机,但也属于跨域。
浏览器执行javascript脚本时,会检查这个脚本属于哪个页面,如果不是同源页面,就不会被执行。
1.2为什么要有跨域限制
跨域限制主要是为了安全考虑
AJAX同源策略主要用来防止CSRF攻击。如果没有AJAX同源策略,相当危险,我们发起的每一次HTTP请求都会带上请求地址对应的cookie,那么可以做如下攻击:
1. 用户登录了自己的银行页面 [http://mybank.com](https://www.jianshu.com/u/9f1d1dcbe846),[http://mybank.com](https://www.jianshu.com/u/9f1d1dcbe846)向用户的cookie中添加用户标识。
2. 用户浏览了恶意页面 [http://evil.com](https://www.jianshu.com/u/9f1d1dcbe846)。执行了页面中的恶意AJAX请求代码。
3. [http://evil.com](https://www.jianshu.com/u/9f1d1dcbe846)向[http://mybank.com](https://www.jianshu.com/u/9f1d1dcbe846)发起AJAX HTTP请求,请求会默认把[http://mybank.com](https://www.jianshu.com/u/9f1d1dcbe846)对应cookie也同时发送过去。
4. 银行页面从发送的cookie中提取用户标识,验证用户无误,response中返回请求数据。此时数据就泄露了。
5. 而且由于Ajax在后台执行,用户无法感知这一过程。
DOM同源策略也一样,如果iframe之间可以跨域访问,可以这样攻击:
1. 做一个假网站,里面用iframe嵌套一个银行网站 [http://mybank.com](https://www.jianshu.com/u/9f1d1dcbe846)。
2. 把iframe宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
3. 这时如果用户输入账号密码,我们的主网站可以跨域访问到[http://mybank.com](https://www.jianshu.com/u/9f1d1dcbe846)的dom节点,就可以拿到用户的输入了,那么就完成了一次攻击。
所以说有了跨域跨域限制之后,我们才能更安全的上网了。
二、跨域的解决方式
2.1跨域资源共享
//url(必须),options(可选)
fetch('/some/url',{
method: 'get',
}).then(function(reponse){
}).catch(function(err){
//出错了;等价于 then 的第二个参数,但这样更好用更直观
})
CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。详见我的另一篇文章CORS
cors为什么就能支持跨域通信呢?
---浏览器会拦截ajax请求如果这个请求是跨域的 ,他会在http头中加 orign
2.2 JSONP
JSONP(JSON with Padding)是资料格式 JSON 的一种“使用模式”,可以让网页从别的网域要资料。
JSONP也叫填充式JSON
,是应用JSON的一种新方法,只不过是被包含在函数调用中的JSON,例如:
callback({"name","getName"});
JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数,而数据就是传入回调函数中的JSON数据。
2.2.1 JSONP原理
在js中,我们直接用XMLHttpRequest
请求不同域上的数据时,是不可以的。但是,在页面上引入不同域上的js脚本文件却是可以的,jsonp正是利用这个特性来实现的。
翻译过来就是通过动态创建script标签,然后利用src属性进行跨域。
<script type="text/javascript">
function getName(jsondata){
//处理获得的json数据
}
</script>
<script src="http://example.com/data.php?callback=getName"></script>
js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入。所以jsonp是需要服务器端的页面进行相应的配合的。
<?php
$callback = $_GET['callback'];//得到回调函数名
$data = array('a','b','c');//要返回的数据
echo $callback.'('.json_encode($data).')';//输出
?>
最终,输出结果为:getName(['a','b','c']);
如果你的页面使用jquery,那么通过它封装的方法就能很方便的来进行jsonp操作了。
<script type="text/javascript">
$.getJSON('http://example.com/data.php?callback=?,function(jsondata)'){
//处理获得的json数据
});
</script>
jquery会自动生成一个全局函数来替换callback=?
中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。$.getJSON
方法会自动判断是否跨域,不跨域的话,就调用普通的ajax方法;跨域的话,则会以异步加载js文件的形式
来调用jsonp
的回调函数。
下面这段JS可能会有助更好的理解JSONP跨域原理
// 定义一个fun函数
function fun(fata) {
console.log(data);
};
// 创建一个脚本,并且告诉后端回调函数名叫fun
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.type = 'text/javasctipt';
script.src = 'demo.js?callback=fun';
body.appendChild(script);
返回的js脚本,直接会执行。所以就执行了事先定义好的fun函数了,并且把数据传入了进来。
fun({"name": "name"})
2.2.2 JSONP的优缺点
JSONP的优点是:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好
,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果
。
JSONP的缺点则是:它只支持GET
请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用
的问题。
为什么JSONP只支持GET方法?
因为基本原理就是通过动态创建script标签,然后利用src属性进行跨域。
总结起来就是一句话:因为<script>标签,只支持GET,你觉得js文件可以post吗?
2.3 服务器代理
浏览器有跨域限制,但是服务器不存在跨域问题,所以可以由服务器请求所要域的资源再返回给客户端。
服务器代理是万能的。
2.4 document.domain来跨子域
浏览器都有一个同源策略,其限制之一就是第一种方法中我们说的不能通过ajax的方法去请求不同源中的文档。 它的第二个限制是浏览器中不同域的框架之间是不能进行js的交互操作的。
不同的框架之间是可以获取window对象的,但却无法获取相应的属性和方法。比如,有一个页面,它的地址是http://www.example.com/a.html , 在这个页面里面有一个iframe,它的src是http://example.com/b.html, 很显然,这个页面与它里面的iframe框架是不同域的,所以我们是无法通过在页面中书写js代码来获取iframe中的东西的:
<script type="text/javascript">
function test(){
var iframe = document.getElementById('ifame');
var win = document.contentWindow;//可以获取到iframe里的window对象,但该window对象的属性和方法几乎是不可用的
var doc = win.document;//这里获取不到iframe里的document对象
var name = win.name;//这里同样获取不到window对象的name属性
}
</script>
<iframe id = "iframe" src="http://example.com/b.html" onload = "test()"></iframe>
这个时候,document.domain
就可以派上用场了,我们只要把http://www.example.com/a.html ,和 http://example.com/b.html,这两个页面的document.domain
都设成相同的域名就可以了。但要注意的是,document.domain
的设置是有限制的,我们只能把document.domain
设置成自身或更高一级的父域,
且主域必须相同。
1.在页面 http://www.example.com/a.html 中设置document.domain:
<iframe id = "iframe" src="http://example.com/b.html" onload = "test()"></iframe>
<script type="text/javascript">
document.domain = 'example.com';//设置成主域
function test(){
alert(document.getElementById('iframe').contentWindow);//contentWindow 可取得子窗口的 window 对象
}
</script>
2.在页面 http://www.example.com/b.html 中也设置document.domain:
<script type="text/javascript">
document.domain = 'example.com';//在iframe载入这个页面也设置document.domain,使之与主页面的document.domain相同
</script>
修改document.domain的方法只适用于不同子域的框架间的交互。
2.5 window.name
window.name 传输技术,原本是 Thomas Frank 用于解决 cookie 的一些劣势(每个域名 4 x 20 Kb的限制、数据只能是字符串、设置和获取 cookie 语法的复杂等等)而发明的。后来 Kris Zyp 在此方法的基础上强化了 window.name 传输 ,用来解决跨域数据传输问题。
window.name 的美妙之处:name值在不同的页面(甚至不同域名)加载后依旧存在
,并且可以支持非常长的 name 值(2MB)。
** window.name 传输技术的基本原理:**
当在浏览器中打开一个页面,或者在页面中添加一个iframe时即会创建一个对应的window对象,当页面加载另一个新的页面时,window的name属性是不会变的。
这样就可以利用在页面动态添加一个iframe然后src加载数据页面
,在数据页面将需要的数据赋值给window.name。
然而此时承载iframe的parent页面还是不能直接访问,不在同一域下iframe的name属性,这时只需要将iframe再加载一个与承载页面同域的空白页面,即可对window.name进行数据读取
示例:
//www.test.com下a.html页:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<button>click!</button>
<script type="text/javascript">
var a=document.getElementsByTagName("button")[0];
a.onclick=function(){ //button添加click事件
var inf=document.createElement("iframe"); //创建iframe
inf.src="http://www.domain.com/window.name/b.html"+"?h=5" //加载数据页www.domain.com/window.name/b.html同事携带参数h=5
var body=document.getElementsByTagName("body")[0];
body.appendChild(inf); //引入a页面
inf.onload=function(){
inf.src='http://www.test.com/b.html' //iframe加载完成,加载www.test.com域下边的空白页b.html
console.log(inf.contentWindow.name) //输出window.name中的数据
body.removeChild(inf) //清除iframe
}
}
</script>
</body>
</html>
//www.domain.com下b.html页:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<a href="" target="" title="">2</a>
<script type="text/javascript" src="../cross/jquery-2.2.3.js">
</script>
<script>
var str=window.location.href.substr(-1,1); //获取url中携带的参数值
$.ajax({
type:"get",
url:"http://www.domain.com/a.php"+"?m="+str, //通过ajax将查询参数传给php页面
async:true,
success:function(res){
window.name=res //将接收到的查询数据赋值给window.name
},
error:function(){
window.name='error' //..
}
});
</script>
</body>
</html>
在www.test.com 下a页面点击button,完美跨域获取到了www.domain.com 下b页面通过ajax请求从a.php得来的数据。
2.6 location.hash跨域
javascript 中 location.hash
是指 www.aa.com/a.php#abc 中的#abc
。
而Hash的改变页面不会刷新
如何跨域
如果index页面要获取远端服务器的数据,动态插入一个iframe,将iframe的src属性指向服务端地址。这时top window和包裹这个iframe的子窗口是不能通信的(同源策略),所以改变子窗口的路径就行了,将数据当做改变后的路径的hash值加在路径上,然后就能通信了(和window.name跨域几乎相同),将数据加在index页面地址的hash值上。index页面监听地址的hash值变化(html5有hashchange事件,用setInterval不断轮询判断兼容ie6/7),然后做出判断,处理数据。
利用hash,场景是当前页面A 通过iframe或frame嵌入了跨域的页面B
//在A中伪代码如下:
var B = document.getElementsByTagName('frame');
B.src = B.src + '#' + data;
//在B中伪代码如下:
window.onhashchange = function(){
var data = window.location.hash;
}
2.7 postmessage
//窗口A(http://A.com)向跨域的窗口B(http://B.com)发送信息
Bwindow.postMessage('data','http://B.com');
//在窗口B中监听
window.addEventListener('message',function(event){
console.log(event.origin);// http://A.com 这样我们就可以选择接受不同域名发送过来的消息了
console.log(event.source)// Awindow
console.log(event.data)// data!
},false)
2.8 WebSocket
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
关于WebSocket详见我的另一篇文章WebSocket
参考资料:
《详解js跨域问题》
网友评论