这次全了,8种超详细Web跨域解决方案!
导语 | 在日常开发过程中,我们通常都会遇到ajax跨域请求或者前端跨域通信的开发场景。无论是前端同学还是后端同学都需要具备解决跨域问题的能力。本文总结梳理了常见的跨域场景、跨域解决方案及其优缺点,希望可以作为大家解决跨域问题的参考。
一、什么是跨域
当a.qq.com域名下的页面或脚本试图去请求b.qq.com域名下的资源时,就是典型的跨域行为。跨域的定义从受限范围可以分为两种,广义跨域和狭义跨域。
(一)广义跨域
广义跨域通常包含以下三种行为:
资源跳转:a链接、重定向。
资源嵌入:<link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链。
脚本请求:浏览器存储数据读取、dom和js对象的跨域操作、js发起的ajax请求等。
其中,资源跳转和资源嵌入行为可以正常请求到跨域资源,脚本请求在未经任何处理的情况下,通常会有跨域问题。
(二)同源策略
同源策略(Same origin policy, SOP)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源策略是指“协议+域名+端口”三者相同。
(三)狭义跨域
狭义跨域正是浏览器同源策略限制的一类请求场景,即我们通常说的跨域行为,通常包含以下三种行为:
cookie、localStorage和indexDB无法读取。
dom和js对象无法获取和操作。
ajax请求无法发送。
二、常见跨域场景
三、跨域解决方案
(一)ajax跨域请求解决方案
日常开发过程中,绝大多数前端页面都会向后端发送ajax请求进行数据交互。那么,ajax请求遇到跨域问题,如何进行解决呢。本文总结以下四种常见解决方案:
jsonp跨域
jsonp (JSON with Padding),是JSON的一种“使用模式”,可以让网页跨域读取数据。其本质是利用script标签的开放策略,浏览器传递callback参数到后端,后端返回数据时会将callback参数作为函数名来包裹数据,从而浏览器就可以跨域请求数据并定制函数来自动处理返回数据。
jsonp跨解实现流程:
jsonp跨域代码示例:
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参callback给后端,后端返回时执行这个在前端定义的回调函数
script.src = 'http://a.qq.com/index.php?callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
jsonp跨域优点:
jsonp兼容性强,适用于所有浏览器,尤其是IE10及以下浏览器。
jsonp跨域缺点:
没有关于调用错误的处理。
只支持GET请求,不支持POST请求以及大数据量的请求,而且也无法拿到相关的返回头,状态码等数据。
callback参数恶意注入,可能会造成xss漏洞。
无法设置资源访问授权。
跨域资源共享(CORS)
跨域资源共享(Cross-origin resource sharing,CORS)是一个 W3C标准,允许浏览器向跨域服务器发送请求,从而克服了ajax只能同源使用的限制。CORS需要浏览器和服务器同时支持。目前,所有主流浏览器(IE10及以上)使用XMLHttpRequest对象都可支持该功能,IE8和IE9需要使用XDomainRequest对象进行兼容。
CORS整个通信过程都是浏览器自动完成,浏览器一旦发现ajax请求跨源,就会自动在头信息中增加Origin字段,用来说明本次请求来自哪个源(协议+域名+端口)。因此,实现CORS通信的关键是服务器,需要服务器配置Access-Control-Allow-Origin头信息。当CORS请求需要携带cookie时,需要服务端配置Access-Control-Allow-Credentials头信息,前端也需要设置withCredentials。
浏览器将CORS请求分成两类:简单请求和非简单请求。简单请求需要满足以下两大条件:
请求方法是以下三种方法之一:HEAD、GET、POST。
HTTP的头信息不超出以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain。
CORS简单请求跨域实现流程:
CORS简单请求跨域代码示例:
// IE8/9需用XDomainRequest兼容
var xhr = new XMLHttpRequest();
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://a.qq.com/index.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=saramliu');
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
CORS跨域优点:
支持所有类型的HTTP请求,功能完善。
通过onerror事件监听进行调用错误处理;
通过Access-Control-Allow-Origin进行资源访问授权。
CORS跨域缺点:
目前主流浏览器(IE10及以上)都支持CORS,但IE8和IE9需要使用XDomainRequest对象进行兼容,IE7及以下浏览器不支持。
Flash跨域(仅供IE7及以下浏览器参考使用)
由于IE7及以下浏览器默认是不兼容跨域请求的,那么在不改造后端的情况下,可以考虑使用Flash进行跨域请求。
Flash在进行跨域请求时,默认首先会发送预检请求,检查服务器域名根目录下的crossdomain.xml文件,判断请求域是否合法。如果域名不合法,Flash直接封杀请求;如果域名合法,则发送真实请求,获取数据并返回给前端页面。
corssdomain.xml是目标域下的主策略文件,其文件配置规则如下:
(1)cross-domain-policy
crossdomain.xml的根元素,包含以下子元素:
site-control
allow-access-from
allow-access-from-identity
allow-http-request-headers-from
(2)site-control
是否允许加载其他策略文件,属性值permitted-cross-domain-policies允许的值有:
none,不允许加载策略文件(包括该主策略文件)。
master-only,仅允许加载主策略文件。
by-content-type,仅允许加载 HTTP/HTTPS 协议下Content-Type为text/x-cross-domain-policy的策略文件。
by-ftp-filename,仅允许加载FTP协议下文件名为crossdomain.xml的策略文件。
all,允许加载任何策略文件。
(3)allow-access-from
用于授权数据访问的请求域,具有以下属性:
domain,指定授予权限的域,可以是域名或IP地址。
to-ports,指定授予权限的socket连接端口范围,以逗号分隔的端口列表,端口范围通过在两个端口号之间插入短划线-指定。
secure,指定信息是否经加密传输。
(4)allow-access-from-identity
允许有特定证书的请求域跨域访问目标域上的资源。
(5)allow-http-request-headers-from
用于授权发送用户定义HTTP头的请求域,具有以下属性:
domain,指定授予权限的域,可以是域名或IP地址。
headers,表明允许发送的HTTP头,以逗号分隔。
secure:指明信息是否经加密传输。
crossdomain.xml文件配置示例:
<cross-domain-policy>
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*.qq.com"/>
<allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>
Flash跨域实现流程:
Flash跨域代码示例:
var FlashHelper = new Object();
// 写入Flash对象
FlashHelper.writeFlash = function () {
var swfName = "Flash4AJAX.swf";
div = document.createElement("div");
div.style.height = '0';
div.innerHTML = '<object id="flashDomId" data="' + swfName + '"'
+ ' type="application/x-shockwave-flash"'
+ ' height="0" width="215">'
+ '<param name="movie" value="' + swfName + '">'
+ '<param name="quality" value="high">'
+ '<param name="swliveconnect" value="true">'
+ '<\/object>';
document.body.appendChild(div);
}
FlashHelper.writeFlash();
// 获取Flash对象
FlashHelper.getFlash = function () {
return document.getElementById("flashDomId");
}
// 回调函数
function handleCallback() {
var response = FlashHelper.getFlash().GetVariable("retText");
alert(response);
}
// 发送请求
function request() {
var url = "http://b.qq.com/index.php";
var method = "GET";
var body = "";
var contentType = "application/x-www-form-urlencoded";
var fs = FlashHelper.getFlash();
fs.XmlHttp(url, "handleCallback", method, body, contentType);
}
request();
Flash跨域优点:
在不改动后端的情况下,可以兼容IE7及以下浏览器的ajax跨域问题。
crossdoamin.xml文件可以进行授权访问控制。
Flash跨域缺点:
受限于浏览器对于Flash插件的支持程度。
没有调用错误的处理。
服务器代理
服务器代理,顾名思义即在发送跨域请求时,后端进行代理中转请求至服务器端,然后将获取的数据返回给前端。一般适用于以下场景:
针对IE7及以下浏览器摒弃Flash插件的情况,配置代理接口与前端页面同源,并中转目标服务器接口,则ajax请求不存在跨域问题。
外网前端页面无法访问内网接口,配置代理接口允许前端页面访问,并中转内网接口,则外网前端页面可以跨域访问内网接口。
服务器代理实现流程:
服务器代理优点:
在不使用Flash的情况下,兼容不支持CORS的浏览器跨域请求。
服务器代理缺点:
后端需要一定的改造工作量。
(二)前端跨域通信解决方案
前端跨域通信是指浏览器中两个不符合同源策略的前端页面进行通信。那么,这种跨域问题,如何进行解决呢。本文总结以下四种常见解决方案:
document.domain+iframe
此方案仅适用于主域相同,子域不同的前端通信跨域场景。如下图所示,两个不符合同源策略的页面http://a.qq.com/a.html和http://b.qq.com/b.html,其主域相同为qq.com。a.html嵌套b.html,再都通过js设置document.domain为主域qq.com,则两个页面满足了同源策略,从而实现了跨域通信。
document.domain+iframe方案代码示例:
<!-- A页面 http://a.qq.com/a.html -->
<iframe id="iframe" src="http://b.qq.com/b.html"></iframe>
<script>
document.domain = "qq.com";
var windowB = document.getElementById("iframe").contentWindow;
alert("B页面的user变量:" + windowB.user);
</script>
<!-- B页面 http://b.qq.com/b.html -->
<script>
document.domain = "qq.com";
var user = "saramliu";
</script>
document.domain+iframe方案优点:
实现逻辑简单,无需额外中转页面
document.domain+iframe方案缺点:
仅适用于主域相同,子域不同的前端通信跨域场景
location.hash+iframe
当两个不符合同源策略且主域不同的页面需要进行跨域通信时,可以利用url的hash值改变但不刷新页面的特性,实现简单的前端跨域通信。
通常情况下http://a.qq.com/a.html内嵌不同域的http://b.qq1.com/b.html时,受浏览器安全机制限制,a.html 可以修改b.html的hash值,但b.html不被允许修改不同域的父窗体a.html的hash值。因此,此方案需要一个与a.html同源的http://a.qq.com/c.html来进行中转,此方案实现流程如下图所示:
location.hash+iframe方案代码示例:
<!-- A页面 http://a.qq.com/a.html -->
<iframe id="iframe" src="http://b.qq1.com/b.html"></iframe>
<script>
// 监听c.html传来的hash值
window.onhashchange = function () {
alert("B页面传递数据:" + location.hash.substring(1));
};
</script>
<!-- B页面 http://b.qq1.com/b.html -->
<iframe id="iframe" src="http://a.qq.com/c.html"></iframe>
<script>
// 向c.html传递hash值
var iframe = document.getElementById('iframe');
setTimeout(function() {
iframe.src = iframe.src + '#user=saramliu';
}, 1000);
</script>
<!-- C页面 http://a.qq.com/c.html -->
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 操作同域a.html的hash值,传递数据
window.parent.parent.location.hash = window.location.hash.substring(1);
};
</script>
location.hash+iframe方案优点:
可以解决主域不同的前端通信跨域问题。
hash改变,页面不会刷新。
location.hash+iframe方案缺点:
受部分浏览器安全机制限制,需要额外的同源中转页面,且中转页面需要js逻辑来修改hash值。
通信数据类型及长度均受限,且数据外显在url上,存在一定安全风险。
window.name+iframe
window.name属性的独特之处在于,name值在不同页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的name值(2MB)。
如下图所示,http://a.qq.com/a.html内嵌不同域的http://b.qq1.com/b.html。b.html有数据要传递时,把数据附加到window.name上,然后跳转到一个和a.html同域的http://a.qq.com/c.html。由于a.html和c.html满足同源策略,a.html可以获取c.html的window.name,从而实现了跨域通信。
window.name+iframe代码示例:
<!-- A页面 http://a.qq.com/a.html -->
<iframe id="iframe" src="http://b.qq1.com/b.html"></iframe>
<script>
var state = 0;
var iframe = document.getElementById('iframe');
iframe.onload = function() {
if (state === 1) {
// 第2次onload成功后,读取同域window.name中数据
alert(iframe.contentWindow.name);
} else if (state === 0) {
// 第1次onload成功后
state = 1;
}
};
</script>
<!-- B页面 http://b.qq1.com/b.html -->
<script>
window.name = "这里是B页面!";
window.location = "http://a.qq.com/c.html";
</script>
window.name+iframe方案优点:
可以解决主域不同的前端通信跨域问题。
通信数据类型不受限,且长度可达2MB。
window.name+iframe方案缺点:
需要额外的同源中转页面,但中转页可以为空白页。
postMessage
postMessage是HTML5 XMLHttpRequest Level2中的API,且是为数不多可以跨域操作的window属性之一,它通常用于解决以下方面的问题:
页面和其打开的新窗口的数据传递。
多窗口之间消息传递。
页面与嵌套iframe消息传递。
postMessage是一种安全的跨域通信方法。当a.html获得对b.html的window对象后,a.html调用postMessage方法分发一个MessageEvent消息。b.html通过监听message事件即可获取a.html传递的数据,从而实现跨域通信。postMessage实现流程如下图所示:
postMessage方法的语法如下:
otherWindow.postMessage(message、targetOrigin、[transfer])
otherWindow
目标窗口的window对象,比如iframe的contentWindow属性、执行window.open返回的window对象等。
message
将要发送给其他window的数据。
targetOrigin
指定哪些窗口能接收到消息事件,其值可以是字符串*(表示无限制)或者是“协议+主机+端口号”。
transfer(可选)
是一串和message同时传递的Transferable对象,这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
postMessage方案代码示例:
<!-- A页面 http://a.qq.com/a.html -->
<iframe id="iframe" src="http://b.qq1.com/b.html"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {meesage: "这里是A页面发的消息"};
var url = "http://b.qq1.com/b.html";
// 向B页面发送消息
iframe.contentWindow.postMessage(JSON.stringify(data), url);
};
window.addEventListener("message", function(e) {
alert("B页面发来消息:" + JSON.parse(e.data));
});
</script>
<!-- B页面 http://b.qq1.com/b.html -->
<script>
window.addEventListener("message", function(e) {
alert("A页面发来消息:" + JSON.parse(e.data));
var data = {meesage: "这里是B页面发的消息"};
var url = "http://a.qq.com/a.html";
window.parent.postMessage(JSON.stringify(data), url);
}, false);
</script>
postMessage方案优点:
可以解决多种类型的前端跨域通信问题;
postMessage方案缺点:
兼容性方面相对差一点,IE8及以下浏览器不支持该方法,IE9只支持postMessage传递string类型的数据,而标准的postMessage消息数据可以是任何类型。
四、总结
本文介绍了浏览器受同源策略限制的跨域行为以及常见的跨域场景。总结了跨域问题的经验,并从ajax请求和前端通信两大方向进行梳理常用的跨域解决方法及其优缺点,希望可以作为大家在日常开发解决web跨域问题的参考。如果有描述不当之处,也希望大家随时进行沟通和指正。
参考资料:
1.浏览器的同源策略
2.跨源资源共享(CORS)
3.Cross-domain AJAX using Flash
4.window.postMessage
作者简介
刘孟
腾讯前端开发工程师
腾讯前端开发工程师,毕业于上海大学。目前负责腾讯优联项目的前端开发工作,有丰富的系统平台及游戏营销活动前端开发经验。
推荐阅读
10分钟带你玩转Kafka基于Controller的领导选举!