什么是跨域
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源。
为什么要跨域
因为所有浏览器都实行同源策略。什么是同源呢?两个页面地址中的协议、域名和端口号一致,则表示同源。
而同源策略限制不同源页面的以下几种行为:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 JS 对象无法获得
- AJAX 请求不能发送
怎么跨域
JSONP(JSON with Padding,填充式JSON)
在JS中,我们虽然不能直接用XMLHttpRequest
请求不同域上的数据,但是却可以用<script>
标签在页面上引入不同域上的JS脚本文件,JSONP正是利用这个特性来实现的。
JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数,而数据就是传入回调函数中的JSON数据。
<script type="text/javascript">
function dosomething(jsondata){
//处理获得的json数据
}
</script>
<script src="http://example.com/data.php?callback=dosomething"></script>
首先第一个<script>
标签定义了一个处理数据的函数;
第二个<script>
标签载入一个JS文件,http://example.com/data.php
是数据所在地址,但是因为是当做JS来引入的,所以http://example.com/data.php
返回的必须是一个能执行的js文件;
最后JS文件载入成功后会执行我们在URL参数中指定的函数,并且会把我们需要的JSON数据作为参数传入。所以php应该是这样的:
<?php
$callback = $_GET['callback']; //得到回调函数名
$data = array('a','b','c'); //要返回的数据
echo $callback.'('.json_encode($data).')'; //输出
?>
最终,输出结果为:dosomething(['a','b','c'])
;
从上面可以看出JSONP是需要服务器端的页面进行相应的配合的。
优点:
- 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;
- 能够直接访问响应文本,支持在浏览器与服务器之间双向通信
缺点:
- 只能使用Get请求
- 不能注册success、error等事件监听函数,不能很容易的确定JSONP请求是否失败
- JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击,其安全性无法确保
CORS(Cross-Origin Resource Sharing,跨源资源共享)
- CORS是一个W3C标准,它允许浏览器向跨源服务器,发出
XMLHttpRequest
请求,从而克服了AJAX只能同源使用的限制,目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 - CORS定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
- 实现此功能非常简单,只需由服务器发送一个响应标头即可。
CORS的两种请求
浏览器将CORS分为两种请求,一种是简单请求,另外一种对应的肯定就是非简单请求。
只要同时满足下面两大条件,就属于简单请求:
请求的方法是一下的三种方法之一:
- HEAD
- GET
- POST
HTTP的头信息不超过以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type: 只限于三个值:
application/x-www-form-urlencoded
、multipart/formdata
、text/plain
。
凡是不同时满足以上两种条件,就属于非简单请求。
浏览器对于两种请求处理是不一样的。
简单请求
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在HTTP请求报文首部,增加一个Origin字段。如下:
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面Origin
字段的用来说明本次请求来自哪个源(协议+域名+端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin
指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin
字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest
的onerror
回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin
指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的HTTP响应报文首部信息中,有三个与CORS请求相关的字段,都是以Access-Control-
开头。
Access-Control-Allow-Origin
该字段是必须的,它的值要么是请求Origin
字段,要么是一个*
,表示接受任意域名的请求。
Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true
,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie,删除该字段即可。
值得一提的是,如果想要CORS支持Cookie,不仅要在服务器指定HTTP响应报文首部字段,还需要在AJAX中打开withCredentials
的属性。(jQuery中AJAX设置后面会讲到)
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
有些浏览器在省略withCredentials
设置的时候,还是会发送Cookie。于是,可以显式关闭这个属性。
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Acess-Control-Allow-Origin
不能设置为*
,必须设置成具体的域名,如果是本地调试的话可以考虑设置成null
。
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
字段的值。
非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type
字段的类型是application/json
。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
下面是一段JavaScript脚本:
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
很明显,这是一个非简单请求,使用了PUT方法来发送请求,并且自定义了一个HTTP请求报文的首部字段。
于是,浏览器发现这是一个非简单的请求,就自动发出了一个“预检”请求,要求服务器确认可以这样请求。下面是这个“预检”请求的HTTP头信息。
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin
,表示请求来自哪个源。
除了Origin
字段,“预检”请求的头信息还包括两个特殊字段。
Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS会用到哪些HTTP方法,上面是PUT。
Access-Control-Request-Headers
该字段是一个用逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息。上面的例子是X-Custom-Header
。
于是,服务器收到“预检”请求之后,检查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨域请求,就可以做出回应。
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest
对象的onerror
回调函数捕获。控制台会打印出如下的报错信息。
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服务器回应的其他CORS相关字段如下:
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
对比简单请求服务器响应的CORS字段,发现多了三个:
Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
于是,一旦浏览器通过了“预检”,以后每次浏览器正常的CORS请求,都跟简单请求一样,会有一个Origin
头信息字段。服务器的回应,也都有一个Access-Control-Allow-Origin
头信息字段。如果开启了Cookie设置,那还有一个Access-Control-Allow-Credentials:true
。
postMessage跨域
iframe可以在父页面中嵌入一个子页面,在日常开发中一旦使用,避免不了的就要涉及到不同的iframe页面进行通信的问题,可能是获得其他iframe的DOM,或者是获取其他iframe上的全局变量或方法等等。
同源下的iframe,也就是iframe中的src
属性的URL符合同源的条件,那么通过iframe的contentDocument
和contentWindow
获取其他iframe的DOM或者全局变量、方法都是很简单的事情。
那如果是非同源的两个iframe,单纯的通过变量访问的方式就受到同源限制了。
为了解决这个问题,HTML5引入了一个新的API:postMessage
,主要就是用来解决存在跨域问题的iframe页面之间通信的问题。
下面简单的举一个例子,假如现在有两个不同的页面,A页面的url是http://localhost:4002/parent.html
,B页面的url的是http://localhost:4003/child.html
,现在我把B页面用iframe嵌在A页面下面,代码(精简)是这样子的。现在我要实现的是向子页面B传递一个消息:
A页面代码:
<body>
<h1>A页面</h1>
<iframe src="http://localhost:4003/child.html" id="child">
</iframe>
<script>
window.onload = function() {
document.getElementById("child").contentWindow.postMessage("父页面发来贺电", "http://localhost:4003");
}
</script>
</body>
B页面代码:
<body>
<h1>B页面</h1>
<script>
window.onload = function() {
window.addEventListener("message", function(e) {
//判断信息的来源是否来自于父页面,保证信息源的安全
if(e.source != window.parent) return;
alert(e.data);
});
};
</script>
</body>
结果如图:
postMessage
接受两个参数,一个是要传送的data,另外一个是目标窗口的源,如果想传给任何窗口,可以设置成*
。
目标页面接收信息的时候,使用的是window.addEventListener("message", function() {})
。
通过window.name跨域
window
对象有个name
属性,该属性有个特征:即在一个窗口的生命周期内,窗口载入的所有的页面都是共享一个window.name
的,每个页面对window.name
都有读写的权限,window.name
是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置,并且可以支持非常长的 name
值(2MB)。由于安全原因,浏览器始终会保持 window.name
是String
类型。
1.)a.html:(http://www.domain1.com/a.html
)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
2.)proxy.html:(http://www.domain1.com/proxy.html
)
中间代理页,与a.html同域,内容为空即可。
3.)b.html:(http://www.domain2.com/b.html
)
<script>
window.name = 'This is domain2 data!';
</script>
总结:通过 iframe
的src
属性由外域转向本地域,跨域数据即由iframe的window.name
从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
location.hash + iframe跨域
因为父窗口可以对iframe
进行URL读写,iframe
也可以读写父窗口的URL,URL有一部分被称为hash
,就是#
号及其后面的字符,它一般用于浏览器锚点定位,Server端并不关心这部分,应该说HTTP请求过程中不会携带hash
,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录。此方法的原理就是改变URL的hash
部分来进行双向通信。每个window
通过改变其他window
的location
来发送消息(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash
的值,所以要借助于父窗口域名下的一个代理iframe
),并通过监听自己的URL的变化来接收消息。这个方式的通信会造成一些不必要的浏览器历史记录,而且有些浏览器不支持onhashchange
事件,需要轮询来获知URL的改变,最后,这样做也存在缺点,诸如数据直接暴露在了URL中,数据容量和类型都有限等。下面举例说明:
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash
值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent
访问a页面所有对象。
1.)a.html:(http://www.domain1.com/a.html
)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2.)b.html:(http://www.domain2.com/b.html
)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
3.)c.html:(http://www.domain1.com/c.html
)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
通过document.domain跨域
此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
1.)父窗口:(http://www.domain.com/a.html
)
<iframe id = "iframe" src="http://damonare.cn/b.html" onload = "test()"></iframe>
<script type="text/javascript">
document.domain = 'damonare.cn';//设置成主域
function test(){
alert(document.getElementById('iframe').contentWindow);//contentWindow 可取得子窗口的 window 对象
}
</script>
2.)子窗口:(http://child.domain.com/b.html
)
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>