zoukankan      html  css  js  c++  java
  • 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 跨域的解决方案吧

  • 相关阅读:
    java它 ------ 图形界面(两)
    使用python+flask让你自己api(教程源代码)
    hadoop工作平台梳理
    互斥锁设计,有效的避免死锁
    Cache基础知识OR1200在ICache一个简短的引论
    工作日志2014-08-04
    POSIX 螺纹具体解释(1-概要)
    3.1、Eclipse
    vim cheat sheet
    C++ 学习资料搜寻与学习(第一期)(未完待续)
  • 原文地址:https://www.cnblogs.com/everlose/p/12498150.html
Copyright © 2011-2022 走看看