使用正確的姿勢跨域


請求跨域問題的產生原因是瀏覽器的同源策略(Same origin policy),這是有Netscape提出的一個著名的安全策略,對於瀏覽器而言,它提供了最基本也是最核心的安全功能。它所指的同源是域名、協議、端口都相同。

從wiki百科上截了個例子說明:

從圖中可以看出,三者只要任何一個不相同,都會導致Failure.

什么樣的姿勢跨域,才能成功的跨域?

貼上一份簡單的node代碼,用以說明服務端的情況:

var http = require('http');
var fs = require('fs');

var reg = /(^|\?|&)callback=\w*/;

var MINE_TYPE = {
  'css': 'text/css',
  'html': 'text/html',
  'js': 'text/javascript',
  'txt':'text/plain'
};

http.createServer(function(req,res){
  var _url = req.url;
  var data = _url.indexOf('callback') >=0 ? req.url.match(reg)[0].substr(10)+'("XMLHttpRequest is success")':'XMLHttpRequest is success';
  res.writeHead(200,{'Content-Type':MINE_TYPE['txt'],'Access-Control-Allow-Origin':'http://192.168.1.162:9988'});
  res.end(data);
}).listen(3003,'192.168.1.162');


function _server(port){
  http.createServer(function(req,res){
    var pathname = req.url.substr(1);
    var _ext = pathname.split('.').pop();
    fs.readFile(pathname,'utf-8',function(err,data){
      res.writeHead(200,{'Content-Type':MINE_TYPE[_ext]});
      res.end(data)
    })
  }).listen(port,'192.168.1.162');    
}

_server(8899);
_server(9988);

先以普通的姿勢來跨一次(本文調試使用端口條件處理同源):

html:

<script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.1.0/jquery.js"></script>
<div class="xmlhttprequset">發送請求</div>

js:

$('.xmlhttprequset').click(function(){
    $.get('http://192.168.1.162:3003',function(res){
        console.log(res);
    })
})

我們在瀏覽器url框輸入192.168.1.162:8899/test.html和192.168.1.162:9988/test.html打開兩個頁面,分別點擊頁面上的"發送請求",得到以下結果:

XMLHttpRequest cannot load http://192.168.1.162:3003/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://192.168.1.162:port' is therefore not allowed access.

看到這個錯誤,心里一如既往的爽...那么爽完之后,該解決錯誤了,最常見的是jsonp處理跨域問題:

js:

$('.xmlhttprequset').click(function(){
  $.ajax({
    type:'get',
    dataType:'jsonp',
    url:'http://192.168.1.162:3003',
    success:function(res){
      console.log(res);
    }
  });
})

於是,我們就能夠在控制台看到打印出來的東西了...

那么我不想使用jsonp呢,能否實現跨域呢?答案是肯定的。而且還可以允許9988端口跨域請求,對8899端口進行"丑拒",就是這么任性~ 辦法就是在response頭配置"Access-Control-Allow-Origin"參數的值來控制給不給跨,不給跨,給跨,跨... ←_← 這真的只是回聲.

於是我們將server.js代碼改了改:

http.createServer(function(req,res){
  var _url = req.url;
  var data = _url.indexOf('callback') >=0 ? req.url.match(reg)[0].substr(10)+'("success")':'success';
  res.writeHead(200,{'Content-Type':MINE_TYPE['txt'],'Access-Control-Allow-Origin':'http://192.168.1.162:9988'});
  res.end(data);
}).listen(3003,'192.168.1.162');

然后將jsonp請求的代碼換回get請求的代碼,重啟這個js,切換到瀏覽器,刷新2個頁面,再次點擊請求,效果如下:

:9988

success

:8899

XMLHttpRequest cannot load http://192.168.1.162:3003/. The 'Access-Control-Allow-Origin' header has a value 'http://192.168.1.162:9988' that is not equal to the supplied origin. Origin 'http://192.168.1.162:8899' is therefore not allowed access.
// 強行暴擊

那么現在姿勢明確了,兩個姿勢:正常請求+response設置允許域,jsonp。

jsonp 實現原理

jsonp通過添加一個<script>標簽,將該標簽的src指向請求資源的接口,並且需要在請求中帶上一個callback參數,script的src是不受瀏覽器的同源策略限制的,所以只要后端將數據包裝在這個callback的方法里返回即可,於是我們有了這段代碼:

server.js:

var data = _url.indexOf('callback') >=0 ? req.url.match(reg)[0].substr(10)+'("success")':'success';
// 如果包含callback參數,則將參數以callback的值為方法名包裝成一個執行函數,否則直接返回數據

jsonp在jQuery中的實現為:

var oldCallbacks = [],
    rjsonp = /(=)\?(?=&|$)|\?\?/;

// 默認jsonp設置
jQuery.ajaxSetup({
    jsonp: "callback",
    jsonpCallback: function () {
        var callback = oldCallbacks.pop() || (jQuery.expando + "_" + (nonce++));
        this[callback] = true;
        return callback;
    }
});

jQuery.ajaxPrefilter("json jsonp", function (s, originalSettings, jqXHR) {

    var callbackName, overwritten, responseContainer,
        jsonProp = s.jsonp !== false && (rjsonp.test(s.url) ?
            "url" :
            typeof s.data === "string" &&
            (s.contentType || "")
                .indexOf("application/x-www-form-urlencoded") === 0 &&
            rjsonp.test(s.data) && "data"
        );

    // jsonp 判斷
    if (jsonProp || s.dataTypes[0] === "jsonp") {

        // 生成 callback 名稱
        callbackName = s.jsonpCallback = jQuery.isFunction(s.jsonpCallback) ?
            s.jsonpCallback() :
            s.jsonpCallback;

        // url處理 加入 callback
        if (jsonProp) {
            s[jsonProp] = s[jsonProp].replace(rjsonp, "$1" + callbackName);
        } else if (s.jsonp !== false) {
            s.url += (rquery.test(s.url) ? "&" : "?") + s.jsonp + "=" + callbackName;
        }

        // 方法執行后取數據
        s.converters["script json"] = function () {
            if (!responseContainer) {
                jQuery.error(callbackName + " was not called");
            }
            return responseContainer[0];
        };

        // Force json dataType
        s.dataTypes[0] = "json";

        // 添加callback這個方法
        overwritten = window[callbackName];
        window[callbackName] = function () {
            responseContainer = arguments;
        };

        // 毀屍滅跡處理
        jqXHR.always(function () {

            // 如果之前不存在這個方法,刪除
            if (overwritten === undefined) {
                jQuery(window).removeProp(callbackName);

                // 如果之前就存在這個方法,恢復
            } else {
                window[callbackName] = overwritten;
            }

            // 
            if (s[callbackName]) {

                // 確保安全處理,不影響其他項
                s.jsonpCallback = originalSettings.jsonpCallback;

                // 預留着
                oldCallbacks.push(callbackName);
            }

            // 如果是個函數,攜帶數據調用
            if (responseContainer && jQuery.isFunction(overwritten)) {
                overwritten(responseContainer[0]);
            }

            responseContainer = overwritten = undefined;
        });

        // 歸為script
        return "script";
    }
});

// jsonp實現,生成script及之后移除
jQuery.ajaxTransport("script", function (s) {

    // 這個函數指明了只處理跨域請求
    if (s.crossDomain) {
        var script, callback;
        return {
            send: function (_, complete) {
                script = jQuery("<script>").prop({
                    charset: s.scriptCharset,
                    src: s.url
                }).on(
                    "load error",
                    callback = function (evt) {
                        script.remove();
                        callback = null;
                        if (evt) {
                            complete(evt.type === "error" ? 404 : 200, evt.type);
                        }
                    }
                    );

                // 使用原生DOM操作避免一些 domManip ajax 問題
                document.head.appendChild(script[0]);
            },
            abort: function () {
                if (callback) {
                    callback();
                }
            }
        };
    }
});

簡單過一遍,就是這樣了...然后扔出整理后的,不加各種判斷的,簡單的,只有20行代碼的實現,幫助理解上面這么長長的一堆代碼:

js:

function getInfo(url, callback, _callback){
    url += url.indexOf('?')>=0 ? '&callback=' + callback : '?callback='+callback;  // url處理
    var overWriteContent;
    var script = document.createElement('script');
    script.src = url;
    overWritten = window.dataBack;
    window[callback] = function(){
      overWriteContent = arguments[0];
    };                                                          //  生成callback方法,掛在window下
    document.head.appendChild(script);                          //  添加script標簽
    script.onload = function(e){
      document.head.removeChild(script);                        //  刪除script標簽
      if(overWritten === undefined) delete window[callback];    //  銷毀window下的callback方法
      if(e.type === 'load'){
        _callback(overWriteContent);                            //  帶上數據執行回調
      } else{
        console.error('error:failed to load the resource');
      }
    }
}

如有不正之處,感謝指正,同時歡迎小伙伴們交流討論~

水平較渣,不喜勿噴,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM