JS 跨域方法思考
概述
ajax跨域方法有很多种。常用的有jsonp请求,xhr2,后台代理方式,基于iframe实现跨域。
jsonp请求
ajax 本身是不可以跨域的,通过产生一个 script 标签来实现跨域。因为 script 标签的 src 属性是没有跨域的限制的。
jquery 其实设置了 dataType: 'jsonp' 后,$.ajax 方法就和 ajax XmlHttpRequest 没什么关系了,取而代之的则是 JSONP 协议。
JSONP 是一个非官方的协议,它允许在服务器端集成 Script tags 返回至客户端,通过 javascript callback 的形式实现跨域访问。
jQuery并没有用其它新奇的技术,只不是对 <script src=""/>
做了个封装,以实现jsonp跨域的方式。
我们封装一层jsonp的实现,废话不说,上代码
var jsonp = function (opts) {
//to produce random string
var generateRandomAlphaNum = function (len) {
var rdmString = '';
for (; rdmString.length < len; rdmString += Math.random().toString(36).substr(2));
return rdmString.substr(0, len);
}
var url = typeof opts === 'string' ? opts : opts.url,
callbackName = opts.callbackName || 'jsonpCallback' + generateRandomAlphaNum(10),
callbackFn = opts.callbackFn || function () {};
if (url.indexOf('callback') === -1) {
url += url.indexOf('?') === -1 ? '?callback=' + callbackName :
'&callback=' + callbackName;
}
if (typeof opts === 'object') {
var params = (function(obj){
var str = '';
for(var prop in obj){
str += prop + '=' + obj[prop] + '&'
}
str = str.slice(0, str.length - 1);
return str;
})(opts.data);
url += '&' + params;
}
var eleScript= document.createElement('script');
eleScript.type = 'text/javascript';
eleScript.id = 'jsonp';
eleScript.src = url;
document.getElementsByTagName('HEAD')[0].appendChild(eleScript);
// window[callbackName] = callbackFn;
//return promise
return new Promise(function (resolve, reject) {
window[callbackName] = function (json) {
resolve(json);
}
//onload are executed just after the sync request is comple,
//please use 'onreadystatechange' if need support IE9-
eleScript.onload = function () {
//delete the script element when a request done。
document.getElementById('jsonp').outerHTML = '';
eleScript = null;
};
eleScript.onerror = function () {
document.getElementById('jsonp').outerHTML = '';
eleScript = null;
reject('error');
}
});
};
jsonp({
url: 'http://erp.souche.com/pc/car/carpricetagaction/carPriceInfo.jsonp', /*url写异域的请求地址*/
data: {
carId: '0a499720d77f4e55a0ac490ed115fc4e',
picNum: 5
}
})
.then(function (d) {
console.log(d.data);
});
jsonp的缺点:只支持GET请求,也容易被运营商劫持插入奇怪的广告。
jsonp的优点:能支持老的浏览器。
xhr2
XHR2在众多HTML5新特性中算是比较低调的一个了。我翻了几本HTML5的书,只有《HTML5高级程序设计》弱弱地讲了几页。网上的资料也不多,很大的一个原因我估计是目前有浏览器还压根不兼容,看看下面的兼容列表:
- Chrome 7.0及以上
- Firefox 3.5以上
- Internet Explorer 10及以上
- Opera 12.1及以上
- Safari 5.0及以上
- android broswer 3及以上
- ios safari 5.1及以上
XHR2这么叫多少有点误导的感觉,实际上没有什么XHR1,XHR2这样的概念,XHR2只是一套新的规范,在原有XHR对象上新增了一些功能:跨域访问,全新的事件,还有请求进度以及响应进度。 所以呢,要实现XHR2特性还是使用XMLHttpRequest对象,前提是浏览器要支持XHR2:
http发现这是一个跨源的ajax请求后就自动在头信息添加一个origin字段,服务器根据这个字段判断是否同意这次请求,如果同意这次请求则在响应头中插入字段 Access-Control-Allow-Origin
。
这是常见的一个 http response header 里关于 xhr2 的设置
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, TT, _security_token
Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS, DELETE
Access-Control-Allow-Origin: http://xxx.domain.com
请注意,如果需要请求中带上 cookie 则把 Access-Control-Allow-Credentials
设置为 true。但是如果 Access-Control-Allow-Origin
为 * 时,这么设置将会报错。
优点是支持所有类型的http请求
缺点是 Access-Control-Allow-Origin
这个头所跨的域本人实践写多个域名无效,那么就只能跨一个网站。
后台代理方式
这种方式可以解决所有跨域问题,也就是将后台作为代理,每次对其它域的请求转交给本域的后台,本域的后台通过模拟http请求去访问其它域,再将返回的结果返回给前台。
经典案例见 webpack 的 proxyTable 的配置,笔者本人在 localhost 环境都是使用这个做一层代理。
优点:无论访问的是文档,还是js文件都可以实现跨域。
缺点:也只能是 localhost 环境用用了。
另外注意疫情期间需要开远程代理,和本地这个 proxyTable 的代理还容易产生一些奇怪的毛病。
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那显然是会报错地!代码如下:
www.a.com 上的 a.html
document.domain = 'a.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://script.a.com/b.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
var doc = ifr.contentDocument || ifr.contentWindow.document;
// 在这里操纵b.html
alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue);
};
script.a.com 上的 b.html
document.domain = 'a.com';
问题:
- 安全性,当一个站点(b.a.com)被攻击后,另一个站点(c.a.com)会引起安全漏洞。
- 如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。
并且在新版 chrome 中存在浏览器的安全限制,操作 iframe 页面具体的 DOM 元素等操作,是不被允许的。
postMessage 跨域
此 API 是 H5 提出的跨源通信方案,详细介绍参见 MDN
上面的 iframe 操作,需要在父页面操作子页面,可能经常会遇到浏览器安全限制。
由此引出一个升级版解决方案,核心思路如下 是用父页面发 postMessage 消息给子页面,而子页面已经设置过能与要通信的后台接口直接通信。
- 创建 iframe,src 指向子页面。
- 通过 iframe.contentWindow 属性获取子页面域中的 window 对象。然后通过该 window 对象下的 postMessage 方法发送数据
- 通过监听 window 对象的 message 事件,通过回调函数接收数据源返回的数据
var iframe = document.createElement("iframe");
iframe.style.display="none";
iframe.src="http://localhost:8001"
window.onload = function() {
var data = document.getElementById("data").value;
document.getElementsByTagName("head")[0].appendChild(ifr);
iframe.onload = function(){
iframe.contentWindow.postMessage(JSON.stringify(data), "http://localhost:8001")
}
}
window.addEventListener("message",function(e){
console.log(e)
console.log(e.data)
},false)
- 监听 window 的 message 消息,回调接收父页面传过来的参数。
- 发送请求与后台通信。你需要自己保证子页面已经能和数据接口直接通信
- 访问 window.parent 获取父窗口的引用。然后通过 postMessage 返回数据给父页面
window.addEventListener("message", function(e) {
if(JSON.parse(e.data)){
window.parent.postMessage("我已经收到data:" + e.data, "http://localhost:8000")
}
},false);
此外必须做好身份校验,使用 origin 和 source。否则很容易中跨站点脚本攻击。
结论
实际开发过程中,我的做法是本地用后台代理方式,也就是 node 中转一层去获取数据,关于这一点比方说 webpack 的配置项 proxyTable 就提供了这样的功能。下面提供了一段简单的配置
var devConf = {
env: 'dev',
globalConfig: {
NODE_ENV: JSON.stringify("development"),
TEST: JSON.stringify("/api/get")
},
proxyTable: {
'/api/get': {
target: 'http://test.sqaproxy.xx.com',
changeOrigin: true,
pathRewrite: {
'/api/get': ''
}
}
}
};
那么具体文件中访问 globalConfig 中的 TEST 变量就是代理后的地址
而到了预发和线上时,自然有后端返回的 Access-Control-Allow-Origin
头来做 xhr2 跨域。
但若发现此后端接口已经设置过了 xhr2 跨域,并且 Access-Control-Allow-Origin
并非你目前写的工程的域名,那就参照一下 postMessage 跨域的解决方案吧