跨域不完全探究

請叫我王磊同學發表於2018-12-02

跨域不完全探究

前言

  首先歡迎大家關注我的Github部落格,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。最近工作上事情比較繁多導致部落格一度斷更,為了挽救慘淡的關注量併兼顧自己有限的時間,準備近期多推出一些有關前端的基礎知識學習,一起夯實基礎。希望大家多多關注呀!   

引子  

  跨域問題在Web開發中一直都是一個非常常見的問題,但在日常工作說實話使用頻率並沒有那麼多,再加上重使用輕原理導致我對跨域問題理解的一直非常的淺顯,藉此機會讓我們好好探索一下。

同源策略

  跨域問題的起源於眾所周知的瀏覽器同源策略。作為如今Web安全基石的同源策略,早在上個世紀九十年代由網景公司提出,當時僅僅只是針對於Cookie的訪問,即不同源的網頁之間Cookie不能共享。後來同源策略變得更加嚴格,安全性也逐步提高,升級為:

  1. 不同源域 Cookie、LocalStorage 和 IndexDB 無法讀取。
  2. 不同源域 DOM 無法獲得。
  3. 不同源域 AJAX 請求不能傳送

  說了這麼多同源,那麼何為同源。通常情況下,如果一個源擁有相同的協議(protocol)、主機(host)、埠(port),則稱其為同源。當然我們說了通常情況下,也就意味著這個方面有例外存在,IE毫無疑問的在這個方面承擔了它固有的職責。IE並沒有將埠號加入到同源策略的組成部分,因此http://host:81http://host:80是屬於同源的。雖然同源的安全限制是必要的,但是同時也帶了束縛,假設我確實需要向不同源的地址傳送請求該怎麼辦呢?那就回到了我們所要討論的跨域問題。

跨域解決方案

JSONP

  每每提起跨域問題,第一個想到的就是JSONP了。JSONP與JSON雖然看起來很像,但卻有本質區別 。JSONP全稱是JSON with Padding,並不是一種新的資料格式,而是資料格式JSON的一種“使用模式”。瀏覽器的同源限制對含有src屬性的元素(例如: scriptimg)不起作用,JSONP就是利用了script的這個特性。

基本原理

  前面我們說了JSONP利用的是script標籤,繞過瀏覽器的同源策略,試想我們通過一個URL獲取JSON資料是可能的。然而JSON資料卻是不可執行的,因此JSONP通過將返回的JSON資料用函式包裹來實現執行的目的,這也就是JSONP中P表示padding(包裹)的含義。因為需要函式包裹資料,所以前後端需要提前約定好函式名,我們一般會在請求的URL中帶上函式名稱作為引數。例如請求介面:

http://api.demo.com/data?userid=1&jsonp=parseResponse
複製程式碼

  伺服器會將對應的資料填充到函式parseResponse:

parseResponse({"Name": "小明", "Id" : 1823, "Rank": 7})
複製程式碼

概念實現

  我們實現一個最簡單的函式用來建立一個JSONP請求:

function createJSONPRequest(url, callbackName){
    var script = document.createElement("script");
    script.src = url +"?callback=handleResponse"; 
    document.body.appendChild(script);
}

function handleResponse(res){
    console.log(res);
}

createJSONPRequest("http://localhost:3001/jsonp", "parseResponse")
複製程式碼

然後我們用express.js來響應這個JSONP請求:

// 伺服器埠號:3001
app.get('/jsonp', (req, res) => {
	var callbackName = req.query.callback;
	var mockData = {
	  name: "MrErHu"
	};
	res.send(`${callbackName}(${JSON.stringify(mockData)})`);
})
複製程式碼

執行時你會發現瀏覽器會正確列印出跨域訪問的資料,說明我們的跨域請求成功。

實踐

  當然我們並不需要在前端手動去實現該函式,很多第三方庫已經封裝了JSONP的請求,我們以JQuery提供的Ajax理由為例,上面的請求用JQuery去實現程式碼是:

// 客戶端埠號:3000
$.ajax({
    url: "http://localhost:3001/jsonp",
    type: "GET",
    dataType: "jsonp",
    jsonpCallback: "parseResponse",
    success: function (res) {
        console.log(res);
    }
});
複製程式碼

  我們可以看到在JQuery中提供的ajax函式中通過配置dataTypejsonp,則可以實現JSONP請求,需要注意的是JQuery只是將其封裝在ajax函式內,二者實質上有本質區別。

  瞭解JSONP的實現原理,我們知道這玩意肯定不會存在瀏覽器相容性問題,畢竟我不相信會存在不支援script標籤的瀏覽器。但是因為正是通過script標籤實現的,JSONP也就只能支援GET請求,那麼如果我們想實現POST請求的跨域有什麼好的辦法嗎?

CORS

  CORS是Cross-origin resource sharing的縮寫,即跨域資源共享,屬於W3C標準,允許跨域傳送XMLHttpRequest請求,支援多種HTTP Method。與JSONP的原理不同的是,CORS採用的是前後端HTTP表頭協商的方式(我自己起的名字)判斷是否允許跨域訪問,並且相比與JSONP來說,CORS需要客戶端和服務端共同支援,並且整個過程由瀏覽器自動完成。對於前端開發者而言,跨域的CORS通訊與普通的Ajax通訊沒有差別,整個跨域處理的過程主要集中在伺服器端。CORS通訊分成簡單請求(simple request)和非簡單請求(not-so-simple request)兩種模式。

簡單請求

  所謂的簡單請求是指,請求方法屬於: HEADGETPOST之一,並且HTTP的頭資訊不超出以下幾種欄位:

Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:僅限於三個值:application/x-www-form-urlencoded、multipart/form-data、text/plain
複製程式碼

  如果不滿足以上任一條件,均屬於非簡單請求。對於簡單請求,瀏覽器會直接傳送CORS請求。當我們使用Ajax傳送一條跨域請求,瀏覽器會在HTTP請求頭(Request Headers)中增加Origin屬性:

跨域不完全探究

  Origin屬性用來表明當前的請求來自於哪個源(協議、主機、埠)。如果服務端支援CORS跨域,則需要在響應頭中返回以下屬性:

Access-Control-Allow-Origin: 
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
複製程式碼
  • Access-Control-Allow-Origin: 表示跨域的訪問的源,其中"*"表示允許來自任何源的請求跨域訪問
  • Access-Control-Allow-Credentials: 表示跨域訪問是否允許帶有Cookie資訊,該值僅可以允許設定為true。如果伺服器不允許請求攜帶Cookie,則不需要傳送該屬性。值得注意的是,攜帶的Cookie資訊僅僅只是所跨域的伺服器域名設定的Cookie,不能攜帶其他域名下的Cookie資訊,Cookie仍然循序同源策略。並且還需要滿足Access-Control-Allow-Origin指定的域與當前的請求的域完全一致(不能是"*")以及在XMLHttpRequest顯式設定withCredentials屬性為true,表示瀏覽器也允許傳送Cookie。
  • Access-Control-Expose-Headers: 該欄位可選,表示瀏覽器可以從該XMLHttpRequest請求中拿到的響應頭資訊。預設僅能拿到Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma這六個屬性,如果想取得其他的屬性,必須在該屬性中指定。

我們用express.js模擬一下:

// 伺服器埠號:3001
app.post('/cors', (req, res) => {
	var mockData = {
		name: "MrErHu"
	};
	var origin = req.get("origin");
	res.set("Access-Control-Allow-Origin", origin);
	res.set("Access-Control-Allow-Credentials", true);
	res.send(JSON.stringify(mockData))
});
複製程式碼

  當我們前端Ajax請求:

// 瀏覽器埠號:3000
$.ajax({
    url: "http://localhost:3001/cors",
    type: "POST",
    success: function (res) {
        console.log(res);
    }
});
複製程式碼

你會發現該條請求已經不會出現跨域問題,可以正確訪問到資料。

非簡單請求

  對於非簡單請求,瀏覽器會預先傳送一次預檢請求,詢問伺服器是否允許跨域訪問以及是否允許HTTP方法,只有得到肯定答覆後,瀏覽器才會發出正式的HTTP請求。比如我們跨域向伺服器傳送一次PUT請求:

// 瀏覽器埠號:3000
$.ajax({
    url: "http://localhost:3001/cors",
    type: "PUT",
    success: function (res) {
        console.log(res);
    }
});
複製程式碼

  你會發現瀏覽器首先會傳送OPTIONS請求去預檢本次跨域請求。

跨域不完全探究

  當預檢請求檢測了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers屬性後就會正式傳送請求。需要注意的是,不僅僅是上述三個屬性,預檢請求還會返回Access-Control-Max-Age屬性用來表示該條預檢請求的有效期,在有效期內,瀏覽器不會再次傳送預檢請求,僅會使用該條快取的預檢請求。

  我們用express.js來模擬本次請求:

// 伺服器埠號:3001
app.options('/cors', (req, res) => {
	var origin = req.get("origin");
	res.set("Access-Control-Allow-Origin", origin);
	res.set("Access-Control-Allow-Credentials", true);
	res.set("Access-Control-Allow-Methods", "PUT");
	res.set("Access-Control-Max-Age", 24 * 60 * 60 * 1000);
	res.send();
});

app.put('/cors', (req, res) => {
	var mockData = {
		name: "MrErHu"
	};
	var origin = req.get("origin");
	res.set("Access-Control-Allow-Origin", origin);
	res.set("Access-Control-Allow-Credentials", true);
	res.send(JSON.stringify(mockData))
});
複製程式碼

  你就會發現本次PUT跨域請求成功,正確返回資料,達到了我們跨域的要求。

對比

  與JSONP相比,CORS支援更多的HTTP方法並且整個過程由瀏覽器自動完成,但是僅僅只相容IE10及以上的瀏覽器,如果需要相容老版本IE瀏覽器,CORS可能就不適合你了。   

後言

我們這邊文章著重講述了兩種跨域的基本原理,但實際上圍繞跨域問題還有很多值得學習的地方,比如後端對跨域請求的處理,以及前端跨域帶來種種的安全性問題。其實不僅僅是JSONP與CORS,Websocket也能實現跨域請求,以後有機會可以學習一下,最後還是歡迎大家關注我的部落格,水平欠佳,希望體諒,願一同進步。

相關文章