WEB跨域請求

web開發隨着ajax的出來帶來了革命性的變化,它改變了web的數據加載方式讓交互更友好,網絡資源更節省。但最初ajax考慮安全性並無開放跨域請求,隨着H5的到來ajax開放了跨域請求,因此ajax跨域請求存在兼容性,不過如今的瀏覽器大部分都已經支持了。javascript


經常使用跨域請求手段有:php

  1. jsonphtml

  2. FORM到頁面框架前端

  3. HTTP服務器代理java

  4. 服務器腳本轉發nginx

  5. ajaxweb

  6. WebSocketajax

  7. 插件
    json



jsonp跨域

這種方式是早期在ajax不支持跨域請求時的一種替代方案應用很是多,在JQuery類的早期框架都集成了此功能。期原理就是經過HTML的<script>標籤加載一個跨域的請求地址並指定一個隨機回調函數,所鏈接的服務器返回指定的回調函數並增長參數,標籤加載完後會自動執行代碼來完成請求回調,所以jsonp只支持GET請求方式,而且須要服務器做專用處理,典型的示例如:

前端代碼:(域名www.a.cn請求域名www.b.cn)

(function (global) {
    //發送請求
    function request(url, data, callback) {
        function jsonpcall(_data, error) {
            callback(_data, error);
            //清理
            delete global[data['jsonpcallback']];
            document.body.removeChild(script);
        }
        data = data || {};
        //生成全局回調函數
        data['jsonpcallback'] = 'jsonp_' + String(Math.random()).substr(2, 10);
        global[data['jsonpcallback']] = jsonpcall;
        var params = getParamsString(data);
        var script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + params;
        script.onerror = function (event) {
            jsonpcall(null, event);
        };
        document.body.appendChild(script);
    }
    //轉換請求參數
    function getParamsString(data, prefix) {
        var arr = [];
        prefix = prefix || '';
        for (var key in data) {
            var name = '';
            if (!prefix) {
                name = key;
            } else {
                name = prefix + '[' + key + ']';
            }
            if (typeof data[key] === 'object') {
                arr.push(getParamsString(data[key], name));
            } else {
                arr.push(name + '=' + data[key]);
            }
        }
        return arr.join('&');
    }
    global.jsonp = request;
})(window);
//發起請求
jsonp('http://www.b.cn', {id: 12}, function () {
    console.info(arguments);
});


服務端代碼:(域名www.b.cn,以PHP爲例)


<?php
if (empty($_GET['jsonpcallback'])) {
    header('HTTP/1.1 404 Not Found');
    die();
}
//查數據,返回結果
$data = [
    'status' => 'ok',
    'msg' => '操做成功'
];
//輸出結果
echo $_GET['jsonpcallback'] . '(' . json_encode($data, JSON_UNESCAPED_UNICODE) . ');';


這種方式請求不受跨域限制使用方便,兼容性好,很是適用小數據量跨域請求。缺點是隻支持GET請求沒法完成像上傳文件或其它請求方式的操做,服務器響應結果專用性強。




FORM到頁面框架

這種方式在異步上傳文件使用很是多,一般是iframe+form結合經過form的target屬性指定到iframe的name完成異步請求,但iframe有部分移動端存瀏覽器不支持,另外還有一個frameset標籤在H5中已經不支持。

使用這種方式跨域請求兼容性僅僅在PC端較好並且服務端不須要做額外處理,但移動端上使用須要留意用戶平臺是否都支持。典型的示例如:

// 請求處理
function request(form, callback) {
    var iframe = document.createElement('iframe'), name = 'IFRAME-' + String(Math.random()).replace('.', ''), submit = 1;
    iframe.name = name, form.target = name;
    iframe.onload = function () {
        //初次加載
        if (submit === 1) {
            form.submit();
        } else if (submit === 2) {
            try {
                var json = JSON.parse((iframe.contentDocument || iframe.contentWindow.document).body.innerHTML);
                callback(json);
            } catch (err) {
                callback(null, err);
            }
            document.body.removeChild(iframe);
        }
        submit++;
    };
    iframe.onerror = function (event) {
        callback(null, event);
        document.body.removeChild(iframe);
    };
    //不能顯示標籤
    iframe.style.display = 'none';
    document.body.appendChild(iframe);
}

//發送請求
request(document.forms[0], function(){
    console.info(arguments);
});


這種方式間接完成跨域操做,同時對服務端的代碼沒有額外要求,只須要返回一個通用的json串便可,經過這個方式能夠完成GET與POST請求而且還能夠上傳文件,最大的缺點是有少部分瀏覽器不支持。



HTTP服務器代理

這種方式從理論上說不須要開發額外的代碼只須要在HTTP服務器上配置代理轉發便可以知足全部請求的跨域請求,但會額外增長服務器性能開銷,不適合於跨域請求過多的場景,畢竟服務器的資源是有限的。通常使用這種方式的基本上是小項目,大項目這麼玩在硬件上開銷不容忽視。以nginx爲例,典型的配置代碼有:(僅以server塊爲例)

    location /proxy {
          # resolver 114.114.114.114 223.5.5.5 valid=3600s;
          proxy_pass http://www.b.com:8099/;
    }

nginx的proxy_pass作代理配置很容易,配置靈活。

  • 若是在proxy_pass中指定了詳細地址則只轉發到對應的地址

  • 若是沒有指定地址則把當前的地址追加到域名下

  • 若是須要指定DNS解析地址則能夠增長resolver命令,但必定要保證DNS服務器正常,不然請求會卡住

  • 轉發可使用IP地址和域名,若是有特定端口必定要追加上

  • 域名可使用變量來傳入,例如:set $proxy_host "http://www.b.com:8099/";  proxy_pass $proxy_host;

  • 能夠指定路徑,也能夠經過if來判斷

nginx配置代理調試比較麻煩須要多注意兩個日誌文件。對於配置要考慮:

  • 是否被截取即沒有進入代理塊

  • 代理域名端口協議是否正確

  • 域名或地址是否能正常訪問



服務器腳本轉發

這種方式是經過服務器的腳原本轉發,避開前端跨域請求,開發方便,對於配置HTTP服務器有限制或問題時是一個比較簡單的替代方案。本質與HTTP服務器代理相似,都是經過服務器來轉來,不可避免增長服務器的開銷。這種處理方式惟一的好處是開發調試容易,兼容性好。以PHP爲例,典型的示例代碼如:

$url = $_GET['_url_'];
$input = file_get_contents('php://input');
$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => FALSE, // 不驗證證書
    CURLOPT_SSL_VERIFYHOST => FALSE, // 不驗證域名
]);
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $input);
    curl_setopt($curl, CURLOPT_URL, $url);
} else {
    unset($_GET['_url_']);
    $params = http_build_query($_GET);
    curl_setopt($curl, CURLOPT_URL, $url . (strpos($url, '?') ? '&' : '?') . $params);
}
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
header('HTTP/1.1 ' . $httpCode);
echo $response;

代碼中簡單的示例經過curl發起跨域請求,前端只須要指定須要請求的跨域地址和參數便可完成請求操做,因爲請求類型多樣化,因此使用代碼轉發要作到兼容全部請求形式則須要作較多的處理,通常用不上。




ajax

ajax支持跨域能夠說能夠很是理想,遺憾的時推出的晚,不過如今隨着瀏覽器全面支持H5使得ajax跨域請求變得普及起來,若是應用平臺涉及比較老的瀏覽器則須要留意了。官方文檔:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS 在官方文檔中有比較詳細的說明。

其實從本質上說跨域限制只是瀏覽器出於安全考慮的一個限制,而開放這個限制也須要合理安全,不然會形成比較多的跨域請求漏洞,所以瀏覽器對ajax跨域請求做了一些限制,它必須要服務器返回容許跨域響應頭信息,才正常提供響應結果,因此ajax跨域請求須要服務端額外增長響應頭信息。

ajax跨域請求分爲兩種場景:基本請求,預檢請求;而且XMLHttpRequest內部經過指定條件強制判斷識別並做出相應的操做,其中預檢請求會觸發CORS預檢,CORS預檢會把請求類型強制修改成OPTIONS類型向服務器發起跨域請求檢查服務器是否響應容許跨域請求頭信息若是容許則再次發起指定的請求類型跨域請求並把響應內容注入到ajax響應內容中,而基本請求是直接發起指定請求類型的跨域請求。兩種請求場景均須要跨域服務器返回容許跨域響應頭信息,響應狀態等頭信息不會受影響。

常規請求

這種場景瀏覽器會直接發起請求,當響應頭信息內容中不包含Access-Control-Allow-Origin容許頭信息時會丟棄響應內容並向控制檯發出Failed to load警告信息,提示服務器沒有Access-Control-Allow-Origin響應頭信息請求不被容許。

條件:(需所有知足)

  • 請求類型爲 GET、HEAD、POST

  • 僅設置過Accept、Accept-Language、Content-Language、Content-Type、DPR、Downlink、Save-Data、Viewport-Width、Width頭信息,或使用默認的不設置頭信息

  • Content-Type只能設置爲application/x-www-form-urlencoded、multipart/form-data、text/plain,或者不設置

  • 在請求中沒有使用過XMLHttpRequestUpload事件監聽。XMLHttpRequestUpload是經過XMLHttpRequest對象的upload屬性獲取的一個上傳進度對象,能夠獲取上傳進度數據。

  • 請求中沒有使用ReadableStream對象。ReadableStream大部分瀏覽器還不支持,是一個獲取響應二進度流的對象處理器。

示例代碼:(只在不觸犯以上任何一個條件便可)

(function (global) {
    function request(url, callback, data, method) {
        var xmlHttp = new XMLHttpRequest();
        var params = getParamsString(data || {});
        method = method || 'GET';
        if (method.toUpperCase() === 'POST') {
            xmlHttp.open(method, url, true);
            xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;');
            xmlHttp.send(params);
        } else {
            xmlHttp.open(method, url + (url.indexOf('?') >= 0 ? '&' : '?') + params, true);
            xmlHttp.send(null);
        }
        xmlHttp.onreadystatechange = function () {
            if (xmlHttp.readyState == 4) {
                try {
                    if (xmlHttp.status == 200) {
                        var json = JSON.parse(xmlHttp.responseText);
                        callback(json);
                    } else {
                        callback(null, xmlHttp.responseText);
                    }
                } catch (err) {
                    callback(null, err);
                }
            }
        };
    }
    //轉換請求參數
    function getParamsString(data, prefix) {
        var arr = [];
        prefix = prefix || '';
        for (var key in data) {
            var name = '';
            if (!prefix) {
                name = key;
            } else {
                name = prefix + '[' + key + ']';
            }
            if (typeof data[key] === 'object') {
                arr.push(getParamsString(data[key], name));
            } else {
                arr.push(name + '=' + data[key]);
            }
        }
        return arr.join('&');
    }
    global.ajax = request;
})(window);
//發起請求
ajax('https://www.b.cn/', function () {
    console.info(arguments);
});


預檢請求

這種場景請求瀏覽器會強制把請求類型改成OPTIONS類型發起預檢跨域請求,當響應頭信息內容中不包含Access-Control-Allow-Origin容許頭信息時會丟棄響應內容並向控制檯發出Failed to load警告信息,提示服務器沒有Access-Control-Allow-Origin響應頭信息請求不被容許;若是響應頭信息所有容許跨域請求則瀏覽器會再次發送指定請求類型的跨域請求到服務器並獲取本次響應內容注入到ajax的響應內容中。

條件:(任意條知足)

  • 請求類型指定爲:PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH

  • 設置了Accept、Accept-Language、Content-Language、Content-Type、DPR、Downlink、Save-Data、Viewport-Width、Width以外的頭信息

  • 設置Content-Type是爲application/x-www-form-urlencoded、multipart/form-data、text/plain以外的類型

  • 在請求中使用過XMLHttpRequestUpload事件監聽

  • 請求中使用ReadableStream對象

示例代碼:(強制增長一個特殊頭信息便可)

(function (global) {
    function request(url, callback, data, method) {
        var xmlHttp = new XMLHttpRequest();
        var params = getParamsString(data || {});
        method = method || 'GET';
        if (method.toUpperCase() === 'POST') {
            xmlHttp.open(method, url, true);
            xmlHttp.setRequestHeader('Ajax-Request', '1');
            xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;');
            xmlHttp.send(params);
        } else {
            xmlHttp.open(method, url + (url.indexOf('?') >= 0 ? '&' : '?') + params, true);
            xmlHttp.setRequestHeader('Ajax-Request', '1');
            xmlHttp.setRequestHeader('Content-Type', 'text/html');
            xmlHttp.send(null);
        }
        xmlHttp.onreadystatechange = function () {
            if (xmlHttp.readyState == 4) {
                try {
                    if (xmlHttp.status == 200) {
                        var json = JSON.parse(xmlHttp.responseText);
                        callback(json);
                    } else {
                        callback(null, xmlHttp.responseText);
                    }
                } catch (err) {
                    callback(null, err);
                }
            }
        };
    }
    //轉換請求參數
    function getParamsString(data, prefix) {
        var arr = [];
        prefix = prefix || '';
        for (var key in data) {
            var name = '';
            if (!prefix) {
                name = key;
            } else {
                name = prefix + '[' + key + ']';
            }
            if (typeof data[key] === 'object') {
                arr.push(getParamsString(data[key], name));
            } else {
                arr.push(name + '=' + data[key]);
            }
        }
        return arr.join('&');
    }
    global.ajax = request;
})(window);
//發起請求
ajax('https://www.b.cn/', function () {
    console.info(arguments);
});


攜帶請求資源

在跨域請求時還容許攜帶請求資源如cookie,但必需設置XMLHttpRequest對象的withCredentials屬性爲true,如:

xmlHttp.withCredentials = true;

攜帶的請求資源受,響應頭信息Access-Control-Allow-Origin、Access-Control-Allow-Credentials兩個影響,Access-Control-Allow-Origin必需指定爲當前請求的域名,Access-Control-Allow-Credentials必需設置爲true不然跨域請求攜帶資源失敗即服務器返回的cookie不會被記錄。跨域請求只受Access-Control-Allow-Origin影響,只要Access-Control-Allow-Origin容許當前域名則請求能夠正常獲取響應結果。

注意:若是設置這個值,Access-Control-Allow-Origin指定爲通配符有兼容問題,須要設置爲對應的域名集。


跨域請求響應頭信息

這些頭信息由服務器響應,瀏覽器內核判斷並做相應處理。這些頭信息是預約好的,只須要按規則響應返回給瀏覽便可完成跨域請求。瀏覽器對這些頭信息處理略有差別,不建議設計的太隨意畢竟安全仍是比較重要的。

Access-Control-Allow-Origin

該頭信息是跨域請求中必需響應的,它用來標識服務器容許哪些請求來源域名共享數據,通常若是沒有特別要求最好設置爲指定的開放域名。若是容許任意域名使用*通配符。若是是指定一個域名而且每次請求該響應頭均會變化則須要增長Vary: Origin頭信息來禁止瀏覽器的緩存。


Access-Control-Expose-Headers

該頭信息容許暴露給外部的頭信息,默認只能夠暴露服務器響應Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma頭信息。若是想讓腳本經過XMLHttpRequest對象的getAllResponseHeaders()函數訪問到其它頭信息則能夠在這裏設置,多個以逗號分開也可使用通配符*。注意預檢請求中詢問成功後纔有效。


Access-Control-Max-Age

該狀信息用於預檢請求中Access-Control-Allow-Methods、Access-Control-Allow-Headers信息能夠緩存多久,單位爲秒,每一個瀏覽器中最大秒數有限制,火狐中最大86400秒,谷歌中600秒,若是須要禁用緩存響應返回-1便可。


Access-Control-Allow-Credentials

該頭信息是容許跨域請求中攜帶請求資源主要是cookie。只要在XMLHttpRequest對象中設置withCredentials=true同時Access-Control-Allow-Origin爲當前請求的域名,Access-Control-Allow-Credentials爲true(字符串)便可攜帶資源請求。


Access-Control-Allow-Methods

該頭信息是容許跨域請求類型集,能夠設置爲指定的請求類型如:GET、POST、PUT、DELETE等,也能夠設置爲*通配符容許全部請求類型,默認不設置即容許全部請求。該頭信息僅在預檢請求中有效,標準要求是當發送OPTIONS請求時若是響應中容許當前指定請求類型時會自動再發起一次指定的請求類型請求並對XMLHttpRequest對象做出回調,目前各瀏覽器實現並不統一,有的瀏覽器此參數無效。


Access-Control-Allow-Headers

該頭信息用於開放額外發送給服務器的頭信息,默認一定容許Accept、Accept-Language、Content-Language、Content-Type等常規容許頭信息設置,當指定其它頭信息時瀏覽器啓動預檢請求詢問服務器是否容許額外的這些頭信息設置,若是容許則再發送常規請求。開放的頭信息多個以逗號分開也可使用通配符*(通配符有兼容問題)。若是頭信息不能匹配容許則請求終止再也不發起常規請求而且XMLHttpRequest獲取響應失敗代碼沒法獲取服務器的響應內容(有兼容性問題部分瀏覽器仍是能夠獲取服務器響應內容)。


跨域禁止修改請求頭信息

這些請求頭信息在跨域請求時瀏覽器禁止代碼修改。在非跨域請求則容許修改。

Origin

標記請求來源站點。


Access-Control-Request-Method

用於通知服務器真實的請求類型。


Access-Control-Request-Headers

用於通知服務器請求頭字段集,多個以逗號分開或使用通配符*


兼容性

跨域請求是新開放的特性在兼容性方面差別比較多,通常推薦使用常規請求會更理想,使用預檢模式下不少代碼或服務器均未對OPTIONS請求類型提供支持致使請求失敗。對於瀏覽器內核兼容性可查看:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Browser_compatibility



WebSocket

WebSocket是web引入的全新技術,讓web端也能實現長鏈接,目前兼容性 https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket#%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%BC%E5%AE%B9%E6%80%A7 。websocket容許跨域鏈接而且不須要額外處理,由於是長鏈接,因此web服務器須要額外調整爲長鏈接才能與終端通訊。websocket鏈接操做簡單,但服務器端須要轉換處理方式,通常應用與遊戲、直播類項目較多。


兼容性

WebSocket請求是專門爲H5設計的,目前支付H5的瀏覽器均支持。在部分瀏覽器中https與http頁面中使用ws與wss有安全限制,要求https只能鏈接wss,而http的可使用ws和wss。

通常在https頁面中最好使用wss協議鏈接,若是服務端不支持wss能夠經過nginx進行轉發把ws改成wss。



插件

增長插件能夠擴展web功能。插件不受瀏覽器過多的限制,在跨域請求中能夠很好的發輝。缺點是插件的開發形式不統一,兼容性也不同,目前比較多的是flash,但flash自身的問題已經將慢慢進入歷史舞臺了。

相關文章
相關標籤/搜索