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.htmlhttp://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 消息给子页面,而子页面已经设置过能与要通信的后台接口直接通信。

父页面 http://localhost:8000

  • 创建 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)

子页面 http://localhost:8001

  • 监听 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 跨域的解决方案吧

posted @ 2020-03-15 16:14  Ever-Lose  阅读(225)  评论(0编辑  收藏  举报