前端跨域的處理

由于瀏覽器的同源策略保護機制,瀏覽器不能執(zhí)行來自其他來源的腳本。通過js在不同的域之間進行數(shù)據(jù)傳輸或通信,比如用ajax向一個不同的域請求數(shù)據(jù),或者通過js獲取頁面中不同域的框架中(iframe)的數(shù)據(jù)的操作就叫跨域。

所謂同源,就是指協(xié)議、域名(IP)、端口三者都相同。只要有其中一者不相同,都是跨域,無法進行數(shù)據(jù)的交流。例如:


注意的是,localhost 與 本機的IP 地址也屬于跨域。

瀏覽器執(zhí)行javascript腳本時,會檢查這個腳本屬于哪個頁面,如果不是同源頁面,就不會被執(zhí)行。

解決方法主要有以下幾種:

1、在工程化的前端項目中,在本機進行開發(fā)時,后臺的接口可能會出現(xiàn)跨域,可以設置 webpack 的服務器代理,將本機的請求轉發(fā)到與接口同源的地址。

首先安裝插件:webpack-dev-server,然后再 config 文件夾里找到 index.js 文件,在 dev 對象下的 proxyTable 字段添加對應的需要替換的地址。

module.exports = {

? ...

? devServer: {? ?//本地開發(fā)服務器設置

? ? ...

? ? port: 8080,? ? ?//當前的端口號

? ? proxyTable: {? //服務器代理設置

? ? ? '/v3/admin': {? //需要代理的接口形式

? ? ? ? target: 'http://localhost:8700',? //將當前端口號代理到8700

? ? ? ? changeOrigin: true,

? ? ? ? secure: false

? ? ? }

? ? }

? }

}

以上設置就是將所有包含 '/v3/admin' 字段的接口,從原來的 localhost: 8080 代理到 localhost: 8700 ,變成 'http://localhost: 8700/v3/admin' 從而獲取到 8700 端口的內容。


2、使用 CORS 進行數(shù)據(jù)傳輸。

CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。

它允許瀏覽器向跨源服務器,發(fā)出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。

CORS標準新增了一組 HTTP 首部字段,允許服務器聲明哪些源站有權限訪問哪些資源。另外,規(guī)范要求,對那些可能對服務器數(shù)據(jù)產生副作用的 HTTP 請求方法(特別是GET以外的 HTTP 請求,或者搭配某些 MIME 類型的POST請求),瀏覽器必須首先使用OPTIONS方法發(fā)起一個預檢請求(preflight request),從而獲知服務端是否允許該跨域請求。服務器確認允許之后,才發(fā)起實際的 HTTP 請求。在預檢請求的返回中,服務器端也可以通知客戶端,是否需要攜帶身份憑證(包括Cookies和 HTTP 認證相關數(shù)據(jù))。

CORS需要瀏覽器和服務器同時支持,瀏覽器需要 ie8 以上。

瀏覽器端的寫法:

function CORSRequest(method,url,opation,callback) {

? var xhr = new XMLHttpRequest();

? if ("withCredentials" in xhr) {

? ? // 此時即支持CORS的情況

? ? // 檢查XMLHttpRequest對象是否有“withCredentials”屬性

? ? // “withCredentials”僅存在于XMLHTTPRequest level 2對象里

? } else {

? ? // 否則檢查是否支持XDomainRequest

? ? // XDomainRequest僅存在于IE中,是IE8 和 IE9 用于支持CORS請求的方式

? ? xhr = new XDomainRequest();

? }

? xhr.open(method, url, true);

? if(method=="POST"){? //可以設置不同請求方法的操作,所有的請求都可以分別設置

? ? xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");? //設置請求頭

? ? xhr.send(opation);? //傳輸數(shù)據(jù)到服務器

? }else{

? ? xhr.send();

? }

? xhr.onload = function(){

? ? callback(xhr.responseText);

? }

};

function notice(data) {

? console.log(data)

}

CORSRequest('POST','http://example.com','傳遞給服務器的數(shù)據(jù)',notice);

服務器端的寫法:

Apache:

Apache需要使用mod_headers模塊來激活HTTP頭的設置,它默認是激活的。你只需要在Apache配置文件的, , 或的配置里加入以下內容即可:

Header set Access-Control-Allow-Origin *??

PHP:

<?php

? header("Access-Control-Allow-Origin:*");? //允許所有來源訪問

? header('Access-Control-Allow-Method:POST,GET');? //允許訪問的方式

以上的配置的含義是允許任何域發(fā)起的請求都可以獲取當前服務器的數(shù)據(jù)。當然,這樣有很大的危險性,惡意站點可能通過XSS攻擊我們的服務器。所以我們應該盡量有針對性的對限制安全的來源,例如:

<?php

? header("Access-Control-Allow-Origin: http://blog.csdn.net");

CORS 的優(yōu)點:

CORS與JSONP相比,無疑更為先進、方便和可靠。

1、 JSONP只能實現(xiàn)GET請求,而CORS支持所有類型的HTTP請求。

2、 使用CORS,開發(fā)者可以使用普通的XMLHttpRequest發(fā)起請求和獲得數(shù)據(jù),擁有 onerror 和 onabort 方法,比起JSONP有更好的錯誤處理。

3、 JSONP主要被老的瀏覽器支持,它們往往不支持CORS,而絕大多數(shù)現(xiàn)代瀏覽器都已經支持了CORS。

注意點:

當發(fā)送的請求是非簡單請求,瀏覽器會在正式通信之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)。瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答復,瀏覽器才會發(fā)出正式的XMLHttpRequest請求,否則就報錯。

一般前端框架比如"ExtJS"、"AngularJS", 框架監(jiān)測到訪問的域名可能存在跨域的話會先發(fā)送一個OPTIONS請求,驗證是否可進行通信,如果返回可通信才會真正發(fā)起一個POST、GET請求。

下圖是框架發(fā)起的OPTIONS請求,當如果服務器的Nginx并沒有設置允許跨域請示的時候,它會返回一個405狀態(tài)碼。

axios 框架也會發(fā)送 OPTIONS 請求。

關于CROS 的更多內容,可以進入以下鏈接了解:

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

http://www.ruanyifeng.com/blog/2016/04/cors.html


3、JSONP?

在js中,我們直接用XMLHttpRequest請求不同域上的數(shù)據(jù)時,是不可以的。但是,在頁面上引入不同域上的js腳本文件卻是可以的,jsonp正是利用這個特性來實現(xiàn)的。

JSONP是一種非正式傳輸協(xié)議,該協(xié)議的一個要點就是允許用戶傳遞一個約定的參數(shù)(一般是callback)給服務端,然后服務端返回數(shù)據(jù)時會將這個callback參數(shù)作為函數(shù)名來包裹住JSON數(shù)據(jù),這樣客戶端就可以隨意定制自己的函數(shù)來自動處理返回數(shù)據(jù)了。

比如,有個a.html頁面,它里面的代碼需要利用ajax獲取一個不同域上的json數(shù)據(jù),假設這個json數(shù)據(jù)地址是http://example.com/data.php,那么a.html中的代碼就可以這樣:

我們看到獲取數(shù)據(jù)的地址后面還有一個callback參數(shù),按慣例是用這個參數(shù)名,但是你用其他的也一樣。當然如果獲取數(shù)據(jù)的jsonp地址頁面不是你自己能控制的,就得按照提供數(shù)據(jù)的那一方的規(guī)定格式來操作了。關鍵是參數(shù)值必須是當前頁面 script 設置的數(shù)據(jù)處理回調函數(shù)的名字。

因為是當做一個js文件來引入的,所以http://example.com/data.php返回的必須是一個能執(zhí)行的js文件,所以這個頁面的php代碼可能是這樣的:

最終那個頁面輸出的結果是:

所以通過http://example.com/data.php?callback=dosomething得到的js文件,就是我們之前定義的dosomething函數(shù),并且它的參數(shù)就是我們需要的json數(shù)據(jù),這樣我們就跨域獲得了我們需要的數(shù)據(jù)。

這樣jsonp的原理就很清楚了,通過script標簽引入一個js文件,這個js文件載入成功后會執(zhí)行我們在url參數(shù)中指定的函數(shù),并且會把我們需要的json數(shù)據(jù)作為參數(shù)傳入。所以jsonp是需要服務器端的頁面進行相應的配合的。

知道jsonp跨域的原理后我們就可以用js動態(tài)生成script標簽來進行跨域操作了,而不用特意的手動的書寫那些script標簽。

一個跨域獲取淘寶關鍵字搜索建議的例子:

效果:


優(yōu)點:

不受同源策略的限制;

兼容性更好,在更加古老的瀏覽器中都可以運行,不需要XMLHttpRequest或ActiveX的支持;

請求完畢后可以通過調用callback的方式回傳結果。

缺點:

只支持GET請求而不支持POST等其它類型的HTTP請求;

只支持跨域HTTP請求這種情況,不能解決不同域的兩個頁面之間如何進行JavaScript調用的問題。


4、設置代理服務器

例如www.123.com/index.html需要調用www.456.com/server.php,可以寫一個接口www.123.com/server.php,由這個接口在后端去調用www.456.com/server.php并拿到返回值,然后再返回給index.html,這就是一個代理的模式。相當于繞過了瀏覽器端,自然就不存在跨域問題。

例如:

a.php 后臺獲取跨域的數(shù)據(jù)

<?php

? $url="https://api.douban.com/v2/movie/subject/1764796”;? //接口地址

? $content=file_get_contents($url);

? echo $content;

?>

前端只要將 XMLHttpRequest 的請求地址設置為 'a.php' 就可以。


5、使用 window.name?

window對象有個name屬性,該屬性有個特征:即在一個窗口(window)的生命周期內,窗口載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的權限,window.name是持久存在一個窗口載入過的所有頁面中的,并不會因新頁面的載入而進行重置。

比如:有一個頁面a.html,它里面有這樣的代碼:

再看看b.html頁面的代碼:

a.html頁面載入后3秒,跳轉到了b.html頁面,結果為:

我們看到在b.html頁面上成功獲取到了它的上一個頁面a.html給window.name設置的值。如果在之后所有載入的頁面都沒對window.name進行修改的話,那么所有這些頁面獲取到的window.name的值都是a.html頁面設置的那個值。當然,如果有需要,其中的任何一個頁面都可以對window.name的值進行修改。注意,window.name的值只能是字符串的形式,這個字符串的大小最大能允許2M左右甚至更大的一個容量,具體取決于不同的瀏覽器,但一般是夠用了。

上面的例子中,我們用到的頁面a.html和b.html是處于同一個域的,但是即使a.html與b.html處于不同的域中,上述結論同樣是適用的,這也正是利用window.name進行跨域的原理。

此方法需要與 iframe 配合使用。

比如有一個www.example.com/a.html頁面,需要通過a.html頁面里的js來獲取另一個位于不同域上的頁面www.cnblogs.com/data.html里的數(shù)據(jù)。

data.html頁面里的代碼很簡單,就是給當前的window.name設置一個a.html頁面想要得到的數(shù)據(jù)值。data.html里的代碼:

那么在a.html頁面中,我們怎么把data.html頁面載入進來呢?顯然我們不能直接在a.html頁面中通過改變window.location來載入data.html頁面,因為我們想要即使a.html頁面不跳轉也能得到data.html里的數(shù)據(jù)。答案就是在a.html頁面中使用一個隱藏的iframe來充當一個中間人角色,由iframe去獲取data.html的數(shù)據(jù),然后a.html再去得到iframe獲取到的數(shù)據(jù)。

充當中間人的iframe想要獲取到data.html的通過window.name設置的數(shù)據(jù),只需要把這個iframe的src設為www.cnblogs.com/data.html就行了。然后a.html想要得到iframe所獲取到的數(shù)據(jù),也就是想要得到iframe的window.name的值,還必須把這個iframe的src設成跟a.html頁面同一個域才行,不然根據(jù)前面講的同源策略,a.html是不能訪問到iframe里的window.name屬性的。這就是整個跨域過程。

看下a.html頁面的代碼:

上面的代碼只是最簡單的原理演示代碼,你可以對使用js封裝上面的過程,比如動態(tài)的創(chuàng)建iframe,動態(tài)的注冊各種事件等等,當然為了安全,獲取完數(shù)據(jù)后,還可以銷毀作為代理的iframe。


6、使用 window.postMessage

window.postMessage(message,targetOrigin)? 方法是html5新引進的特性,可以使用它來向其它的window對象發(fā)送消息,無論這個window對象是屬于同源或不同源,目前IE8+、FireFox、Chrome、Opera等瀏覽器都已經支持window.postMessage方法。

調用postMessage方法的window對象是指要接收消息的那一個window對象,該方法的第一個參數(shù)message為要發(fā)送的消息,類型只能為字符串;第二個參數(shù)targetOrigin用來限定接收消息的那個window對象所在的域,如果不想限定域,可以使用通配符 *? 。

需要接收消息的window對象,可是通過監(jiān)聽自身的message事件來獲取傳過來的消息,消息內容儲存在該事件對象的data屬性中。

上面所說的向其他window對象發(fā)送消息,其實就是指一個頁面有幾個框架的那種情況,因為每一個框架都有一個window對象。下面看一個簡單的示例,有兩個頁面

我們運行a頁面后得到的結果:

我們看到b頁面成功的收到了消息。

使用postMessage來跨域傳送數(shù)據(jù)還是比較直觀和方便的,但是缺點是IE6、IE7不支持,所以用不用還得根據(jù)實際需要來決定。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 跨域資源共享 CORS 對于web開發(fā)來講,由于瀏覽器的同源策略,我們需要經常使用一些hack的方法去跨域獲取資源...
    默默先生Alec閱讀 665評論 0 0
  • 前言 關于前端跨域的解決方法的多種多樣實在讓人目不暇接。以前碰到一個公司面試的場景是這樣的,好幾個人一起在等待面試...
    andreaxiang閱讀 526評論 1 4
  • 前言 原文地址:前端跨域總結 博主博客地址:Damonare的個人博客 相信每一個前端er對于跨域這兩個字都不會陌...
    秦至閱讀 1,477評論 4 51
  • 1957年10月4日,人類歷史上第一顆人造衛(wèi)星“史普特尼克”(Sputnik)上天,從那之后,舊火箭,被遺棄的衛(wèi)星...
    偽君子_閱讀 2,395評論 1 0
  • 十年前的今天,我正焦慮、忐忑的等待著我的第二次高考。是的,我第一次高考成績并不理想,差二本線十分。這也與我平時吊兒...
    天才的笨笨閱讀 277評論 0 1

友情鏈接更多精彩內容