跨域问题
什么是同源策略
通常来说,浏览器出于安全方面的考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不许读写对方的资源,本域指的是
- 同协议:比如都是http或者https
- 同域名:比如都是 http://baidu.com/a 和 http://baidu.com/b
- 同端口:比如都是80端口
同源
不同源
- http://baidu.com/main.js 和 https://baidu.com/a.php 协议不同
- http://baidu.com/main.js 和 http://bbs.baidu.com/a.php 域名不同,域名必须完全相同才可以
- http://baidu.com/main.js 和 http://baidu.com:8080/a.php 端口不同,第一个默认是80
对于当前页面来说页面存放JS文件的域不重要,重要的是加载该JS的页面所在什么域
什么是跨域
- 在JavaScript中,有一个很重要的安全性限制,被称为“Same-Origin Policy”(同源策略)。这一策略对于JavaScript代码能够访问的页面内容做了很重要的限制,即JavaScript只能访问与包含它的文档在同一域下的内容
- JavaScript的同源策略在进行多iframe或多窗口编程、以及Ajax编程时显得尤为重要
- 根据这个策略,在baidu.com下的页面中包含的JavaScript代码,不能访问在google.com域名下的页面内容
- 不同的子域名之间的页面也不能通过JavaScript代码互相访问
- 对于Ajax的影响在于,通过XMLHttpRequest实现的Ajax请求,不能向不同的域提交请求,例如,在abc.example.com下的页面,不能向def.example.com提交Ajax请求
注意要点
- 如果是协议和端口造成的跨域问题前台是无能为力的
- 在跨域问题上,域仅仅是通过URL的首部来识别而不会去尝试判断相同的IP地址对应着两个域或两个域是否在同一个ip上。比如
http://www.a.com/a.js
和http://70.32.92.74/b.js
,虽然域名和域名的ip对应,不过还是被认为是跨域 - URL的首部指window.location.protocol +window.location.host,其中
- window.location.protocol:含有URL第一部分的字符串,如:
http:
- window.location.host:包含有URL中主机名 + 端口号 + 部分的字符串,如:
//www.cenpok.net/server/
为什么要实现同源限制
- 比如一个黑客,他利用iframe把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名和密码登录时,如果没有同源限制,他的页面就可以通过JavaScript读取到你的表单中输入的内容,这样用户名和密码就轻松到手了
- 又比如你登录了OSC,同时浏览了恶意网站,如果没有同源限制,该恶意网站就可以构造Ajax请求频繁在OSC发广告帖
常用的跨域处理方法
JSONP(JSON with padding)
原理
在页面上有三种资源是可以与页面本身不同源的
- js脚本
- css样式文件
- 图片
像淘宝等大型网站,肯定会将这些静态资源放入cdn中,然后在页面上连接,如下所示,所以它们是可以链接访问到不同源的资源的
<script type="text/javascript" src="某某cdn地址" ></script>
<link type="text/css" rel="stylesheet" href="某个cdn地址" />
<img src="某个cdn地址" alt=""/>
而jsonp就是利用了script标签的src属性是没有跨域的限制的,从而达到跨域访问的目的。因此它的最基本原理就是:动态添加一个<script>
标签来实现
实现方法
我们使用Ajax来请求的,看起来和Ajax没啥区别,其实还是有区别的
ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加<script>
标签来调用服务器提供的js脚本
$.ajax({
url:"http://crossdomain.com/services.php",
dataType:'jsonp',
data:'',
jsonp:'callback',
success:function(result) {
// some code
}
});
上面的代码中,callback是必须的,callback是什么值要跟后台拿。获取到的jsonp数据格式如下
flightHandler({
"code": "CA1998",
"price": 1780,
"tickets": 5
});
jsonp的全称为json with padding,上面的数据中,flightHandler就是那个padding
优缺点
优点
- 不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制
- 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持
- 在请求完毕后可以通过调用callback的方式回传结果
缺点
- 只能使用get方法,不能使用post方法
- script,link, img等标签引入外部资源,都是get请求的,那么就决定了jsonp一定是get请求
- 但有时候我们使用的post请求也成功,这是因为当我们指定
dataType: 'jsonp'
,不论你指定type: "post"
或者type: "get"
,其实质上进行的都是get请求
- 没有关于JSONP调用的错误处理
- 如果动态脚本插入有效,就执行调用
- 如果无效,就静默失败
- 失败是没有任何提示的
- 例如,不能从服务器捕捉到404错误,也不能取消或重新开始请求
- 只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题
CORS策略
原理
- CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了Ajax只能同源使用的限制
- CORS系统定义了一种浏览器和服务器交互的方式来确定是否允许跨域请求。 它是一个妥协,有更大的灵活性,但比起简单地允许所有这些的要求来说更加安全
实现方法
- CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10
- 整个CORS通信过程,都是浏览器自动完成,不需要用户参与
- 对于开发者来说,CORS通信与同源的Ajax通信没有差别,代码完全一样
- 浏览器一旦发现A请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉
前端实现
以前我们使用Ajax,使用如下代码
var xhr = new XMLHttpRequest();
xhr.open("GET", "/hfahe", true);
xhr.send();
// 这里的“/hfahe”是本域的相对路径。
如果我们要使用CORS,使用如下代码
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://blog.csdn.net/hfahe", true);
xhr.send();
// 请注意,代码与之前的区别就在于相对路径换成了其他域的绝对路径,也就是你要跨域访问的接口地址
服务器方面
服务器端对于CORS的支持,主要就是通过设置Access-Control-Allow-Origin来进行的。如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问
优缺点:
优点
- CORS支持所有类型的HTTP请求
- 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理
缺点
兼容性方面相对差一点,IE10+才支持
document.domain+iframe
只有在主域相同的时候才能使用该方法
原理
- 浏览器中不同域的框架之间是不能进行js的交互操作的,但是不同的框架之间(父子或同辈),是能够获取到彼此的window对象的,但是,我们也只能获取到一个几乎无用的window对象
- 比如,有一个页面,它的地址是 http://www.example.com/a.html , 在这个页面里面有一个iframe,它的src是 http://example.com/b.html , 很显然这个页面与它里面的iframe框架是不同域的,所以我们是无法通过在页面中书写js代码来获取iframe中的东西的
- 这个时候,document.domain就可以派上用场了,我们只要把http://www.example.com/a.html 和 http://example.com/b.html 这两个页面的document.domain都设成相同的域名就可以了
- 但要注意的是,document.domain的设置是有限制的,我们只能把document.domain设置成自身或更高一级的父域,且主域必须相同。例如:a.b.example.com 中某个文档的document.domain可以设成 a.b.example.com、b.example.com 、example.com 中的任意一个,但是不可以设成 c.a.b.example.com,因为这是当前域的子域,也不可以设成baidu.com, 因为主域已经不相同了
实现方法
比如在 http://www.example.com/a.html 的页面里要访问 http://example.com/b.html 里面的东西,则在页面 http://www.example.com/a.html 中设置document.domain
// http://www.example.com/a.html
<html>
<head>
<title>A页面</title>
<script type="text/javascript" src="jquery.js"></script>
</head>
<body>
<div>A页面</div>
<iframe id="iframe" src="http://example.com/b.html" style="display:none;"></iframe>
// 相当于用一个隐藏的iframe来做代理
<script>
$(function(){
try{
document.domain = "example.com"; // 这里将document.domain设置成一样
}catch(e){}
$("#iframe").load(function(){
var iframe = $("#iframe").contentDocument.$;
iframe.get("http://example.com/接口", function(data){});
});
});
</script>
<body>
</html>
在页面 http://example.com/b.html 中也设置document.domain,而且这也是必须的,虽然这个文档的domain就是 example.com,但是还是必须显示的设置document.domain的值
// http://example.com/b.html
<html>
<head>
<title>B页面</title>
<script type="text/javascript" src="jquery.js"></script>
</head>
<body>
<div>B页面</div>
<script>
$(function(){
try{
document.domain = "example.com"; // 这里将document.domain设置成一样
}catch(e){}
});
</script>
</body>
</html>
⚠️注意
- 在A页面中,要等iframe标签完成加载B页面之后,再取iframe对象的contentDocument,否则如果B页面没有被iframe完全加载,在A页面中通过contentDocument属性就取不到B页面中的jQuery对象
- 一旦取到B页面中的jQuery对象,就可以直接发Ajax请求了,这种类似“代理”方式可以解决主子域的跨域问题。
优缺点
只有在主域相同的时候才能使用该方法
HTML5 postMessage
原理
- 一个html5所提供的一个API,HTML5 window.postMessage是一个安全的、基于事件的消息API
- 在需要发送消息的源窗口调用postMessage方法即可发送消息
- 源窗口可以是全局的window对象,也可以是以下类型的窗口
- 文档窗口中的iframe:
var iframe = document.getElementById('my-iframe');
var win = iframe.documentWindow;
- JavaScript打开的弹窗:
var win = window.open();
- 当前文档窗口的父窗口:
var win = window.parent;
- window.opener()
var win = window.opener();
发送消息
找到源window对象后,即可调用postMessage API向目标窗口发送消息
win.postMessage(msg, targetOrigin);
说明
postMessage函数接收2个参数
- msg:将要发送的消息,可以是一切JavaScript参数,如字符串,数字,对象,数组等
- targetOrigin:这个参数称作“目标域”,⚠️注意,是目标域不是本域!比如,你想在2.com 的网页上往 1.com 网页上传消息,那么这个参数就是 http://1.com/ ,而不是 2.com 协议
- 一个完整的域名包括:主机名,端口号,如:http://g.cn:80/
接收消息
只要监听window的message事件就可以接收了
var onmessage = function (event) {
var data = event.data;
var origin = event.origin;
//do someing
};
if (typeof window.addEventListener != 'undefined') {
window.addEventListener('message', onmessage, false);
} else if (typeof window.attachEvent != 'undefined') {
//for ie
window.attachEvent('onmessage', onmessage);
}
message事件监听函数接收一个参数,Event对象实例,该对象有3个属性
- data:消息
- origin:消息的来源地址
- source:发送消息窗口的window对象引用
实现方法
http://test.com/index.html 发送消息的页面
<div>
<!-- 要给下面的页面传一个妹子过去 -->
<iframe id="child" src="http://lsLib.com/lsLib.html"></iframe>
</div>
<script type="text/javascript">
window.onload=function(){
window.frames[0].postMessage('苍老师','http://lslib.com');
}
</script>
http://lslib.com/lslib.html 接收消息的页面
<script type="text/javascript">
window.addEventListener('message',function (e) {
console.log(e.origin, e.data);
alert('收到妹子一枚:' + e.data);
});
</script>
优缺点
优点
- 方便,安全,有效的解决了跨域问题
- 是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源
- 目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。如果是现代浏览器,首选
缺点
IE8+才支持,而且IE8+<IE10只支持iframe的方式
window.name属性
相对比较完美的方法
原理
- 当iframe的页面跳到其他地址时,其window.name值保持不变,并且可以支持非常长的name 值(2MB)
- 浏览器跨域iframe禁止互相调用/传值,但是调用iframe时window.name却不变,正是利用这个特性来互相传值,当然跨域下是不容许读取ifram的window.name值
实现方法
- 准备三个页面
- http://www.a.com/main.html 应用页面
- http://www.a.com/other.html 代理页面,要求和应用页面在同一个域,一般是一个空的html
- http://www.b.com/data.html 应用页面获取数据的页面,简称:数据页面
- 数据页面将数据传到window.name中去
http://www.b.com/data.html 中的data.html
// 可以是其他类型的数据,比如数组,对象等等
window.name="苍老师";
http://www.a.com/main.html 应用页面
- 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)。
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
优缺点
优点
- 主要是应用当iframe的页面跳到其他地址时,其window.name值保持不变的原理
- 兼容性好,需要照顾落后的浏览器时,首选