不再學AJAX了!(三)跨域獲取資源 ② - JSONP & CORS

瀏覽器的「同源策略」當然保障了互聯網世界的數據隱私與數據安全,可是若是當咱們須要使用AJAX跨域請求資源時,「同源策略」又會成爲開發者的阻礙。在本文中,咱們會簡單介紹須要跨域請求資源的兩種情景,而後,詳細解釋目前主流的四種跨域請求資源方案。html

讓咱們開始吧!程序員

1、什麼時候須要跨域

試想,當咱們擁有多個站點,而且這些站點又常常共享相同的數據,那麼爲每一個站點存儲一份數據看起來就蠢透了。更好的方案是,咱們建設一臺靜態資源存儲服務器,而後讓咱們的全部站點都從這一臺服務器上獲取資源。很理想的方案,可是現實中,咱們首要解決的問題即是瀏覽器的「同源策略」,別忘了,不一樣域之間沒法經過AJAX技術獲取資源。這是須要跨域獲取資源的主要情景。web

另外,站在互聯網「開放,平等,自由」精神的角度上講,若是全部人的數據都被設置爲只有同域才能訪問,那麼互聯網世界未免也太無聊了,若是我就是想要與更多的人分享個人數據,難道不該該有辦法讓我作到這一點嗎?json

固然有辦法,下面咱們就將一一解釋當下主流的跨域請求資源方式。api


2、跨域請求資源方案

咱們將主要介紹如下四種跨域請求資源的方案,並逐一解釋他們的原理,實用方式以及優缺點,但願你和我同樣有耐心,耐心老是能帶來回報:跨域

  1. 野路子出身卻好用的方式:JSONP;
  2. 官方推薦的跨域資源共享方案:CORS;
  3. 使用HTML5 API:postMessage;
  4. 拋棄HTTP,使用:Web Sockets;

在開始下面的內容以前,咱們首先須要強調一點,不管是怎樣的跨域資源獲取方案,本質上都須要服務器端的支持。跨域獲取資源之因此可以成功,本質是服務器默許了你有權限獲取相應資源。下面咱們所運用的種種方式,其實是客戶端和服務端互相配合,繞過同源策略進行數據交互的工做,千萬不要誤覺得掌握了下述技術後,咱們就能成爲一個黑客 🤷🏿‍♂️。瀏覽器

(一)野路子出身卻異常好用的方式:JSONP

正如標題所描述的那樣,JSONP技術是早期某個(些?)聰明的程序員發明的跨域資源獲取方式,因爲該技術的簡單易用,逐漸變得愈來愈流行,最終成爲經典的跨域獲取資源方案。緩存

JSONP是「JSON with padding」的簡寫,我將其翻譯爲「被包裹的JSON」,當你看完這個章節,你必定會以爲這個名字至關貼切。安全

讓咱們模擬一下當初想到JSONP技術的高手程序員是如何推理的:服務器

首先,咱們應該清楚的認識到,瀏覽器的「同源策略」只是阻止了經過AJAX技術跨域獲取資源,而並無禁止跨域獲取資源這件事自己,正因如此,咱們能夠經過<link>標籤,<img>標籤以及<script>標籤中的href屬性或src屬性獲取異域的CSS,JS資源和圖片(雖然咱們其實並不能讀取這些資源的內容)。

其次,咱們知道(也許你不知道,可是,還記得嗎,我在模擬那個高手程序員?)<script>標籤經過src屬性加載的JS資源,實際上只是將JS文件內容原封不動的放置在<scritp>的標籤內,並無什麼神奇之處!

也就是說,若是咱們的sayHi.js文件只有這樣一段代碼:

// sayHi.js
alert('Hi')
複製代碼

當咱們在HTML文件中,成功加載sayHi.js文件時,瀏覽器只不過是作了以下操做:

<!-- 加載前 -->
<script src="sayHi.js"></script>

<!-- 加載後 (爲了方便閱讀,我格式化了代碼)-->
<script src="sayHi.js">
    alert('Hi')
</script>
複製代碼

這意味着什麼呢?這意味着被加載的文件與HTML文件下的其餘JS文件共享一個全局做用域。也就是說,<scritp>標籤加載到的資源是能夠被全局做用域下的函數所使用的!

可是慢着!若是<script>標籤加載到的一些數據並不符合JavaScript語法規定的數據類型,JavaScript就沒法處理這些錯誤不是嗎?並且就算數據類型正常了,咱們還應該將數據存儲於一個變量內,而後調用這個變量...

說的沒錯!不過咱們其實已經離正確答案很近了。

還記的咱們這一方案的名稱嗎?JSONP!,也就是說咱們已經約定好了數據的格式爲JSON,這是JavaScript能夠處理的數據類型,而且JSON格式的數據能夠承載大量信息。那麼有關變量的問題呢?這個回答則更巧妙些,由於咱們會經過向服務器傳入一個函數的方式,將數據變爲函數的參數,讓咱們直接看看JSONP的使用方式:

1.    function handleResponse(response) {
2.        alert(`You get the data : ${response}`)
3.    }
4.    const script = document.createElement('script')
5.    script.src = 'http://somesite.com/json/?callback=handleResponse'
6.    document.body.insertBefore(script, document.body.firstChild)
複製代碼

很容易看到,咱們在1-3行中建立了一個函數,該函數用來處理咱們將要得到的數據,該函數的參數response便是服務器響應的數據。在4-6行中咱們所作的是利用JavaScript動態生成一個script標籤,並將其插入HTML文檔。可是注意第5行咱們制定的src值,在URL末尾,咱們有這樣一段查詢參數callback=handleResponse,callback的值正是咱們先前建立的函數。

事情開始變得有些使人困惑了,究竟發生了什麼呢?咱們如何經過上述代碼最終實現跨域獲取資源?

答案就藏在服務端的代碼中,當服務端支持JSONP技術時,會作以下一些設置:

  1. 識別請求的URL,提取callback參數的值,並動態生成一個執行該參數值(一個函數)的JavaScript語句;
  2. 將須要返回的數據放入動態生成的函數中,等待其加在到頁面時被執行;

此時該文件內容看起來就像這樣:

handleResponse(response) // response爲被請求的JSON格式的數據
複製代碼

所以,當資源加載到位,內容顯示在script標籤內時,瀏覽器引擎會執行這條語句,咱們想要的數據就能夠被咱們以任何想要的方式處理了。真難以想象!

你如今知道爲何這項技術被命名爲JSONP了吧?那個「padding」指的就是咱們的「callback」函數,真是恰如其名。

最後,咱們還要對JSONP技術再強調兩點:

  1. JSONP技術與AJAX技術無關:雖然一樣牽扯到跨域獲取資源這個主題,但咱們應該已經清楚的看到,JSONP的本質是繞過AJAX獲取資源的機制,使用原始的src屬性獲取異域資源;
  2. JSONP技術存在一下三點缺陷:
    • 沒法發送POST請求,也就是說JSONP技術只能用於請求異域資源,沒法上傳數據或修改異域數據;
    • 沒法監測JSONP請求是否失敗;
    • 可能存在安全隱患:別忘了,JSONP之因此能成功獲取異域服務器資源,靠的是服務器動態生成了回調函數,並在頁面中執行,那麼若是服務器在原有的回調函數下再添加些別的惡意JavaScript代碼會怎樣?固然也會被執行!因此在使用JSONP技術時,必定要確保請求資源的服務器是值得信賴的;

雖然存在一些缺陷,但JSONP的瀏覽器兼容性倒是很是好的,能夠說是一種很是小巧高效的跨域資源獲取技術。


(二)官方推薦的跨域資源共享方案:CORS

CORS是W3C頒佈的一個瀏覽器技術規範,其全稱爲「跨域資源共享」(Cross-origin resource sharing),它的意義在於,它是由W3C官方推廣的容許經過AJAX技術跨域獲取資源的規範,所以相較於JSONP而言,功能更增強大,使用起來也沒有了hack的味道。

關於CORS的具體細節,我建議你能夠移步阮一峯的同主題博客閱讀,我認爲該文章已經將這個主題講解的十分透徹了。

你固然也能夠選擇繼續向下閱讀,看看我是怎樣理解CORS技術並從新梳理CORS技術相關知識的,但願也能給你帶來幫助。

咱們以前提到過,若是想要繞過瀏覽器「同源策略」,實現使用AJAX技術跨域獲取資源,須要服務端和客戶端的協同合做。而對於CORS標準而言,實現AJAX跨域獲取資源,重點還在於服務器端返回的響應是否清楚的告知了瀏覽器這次跨域AJAX請求的合法性。

那麼?服務器端該如何向瀏覽器傳達這一信息呢?答案是要看AJAX請求的複雜程度,也就是說,對於簡單的AJAX請求,服務器要向瀏覽器作出的「說明」就少,而若是是複雜的AJAX,服務器則要向瀏覽器多「解釋」幾句。

那麼,如何區分AJAX請求的複雜度呢,標準在於簡單的AJAX請求只符合下面兩個條件:

  1. 請求方法只屬於HEADGETPOST請求的其中一種;
  2. HTTP的頭信息只限於如下字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type(只能爲application/x-www-form-urlencodedmultipart/form-datatext/plain其中一種)

而當瀏覽器檢測到一個簡單的跨域AJAX請求,瀏覽器會首先爲咱們添加一個頭部信息:Origin它的值爲請求發送代碼所在的源(但願你還記得,一個由「協議」,「域名端口」組成)。相似這樣:

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0 ...
複製代碼

而當這樣的一條HTTP請求發送到服務端時,服務端會檢測該請求報頭中的Origin字段的值是否在許可範圍內,若是的確是服務端承認的域,那麼服務端會在響應報文中添加以下字段:

  • Access-Control-Allow-Origin(必須):該字段用來告知瀏覽器服務端接受的可以發送跨域AJAX請求的域,它的值要麼是該次AJAX請求報頭中由瀏覽器自動添加的Origin值,要麼還能夠是一個*號,表示能夠接受任意的域名請求;
  • Access-Control-Allow-Credentials(可選):該字段用來告知瀏覽器是否容許客戶端向服務端發送Cookie。默認狀況下,CORS規範會阻止跨域AJAX向服務端發送Cookie,所以該字段默認值爲false,當你顯式的將該字段值設置爲true時,則表示容許這次跨域AJAX向服務端發送Cookie。
  • Access-Control-Expose-Headers(可選):該字段用來向客戶端暴露可獲取的響應頭;

CORS規範規定,客戶端XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本的字段: * Cache-Control:表示響應遵循的緩存機制; * Content-Language:表示響應體的語言; * Content-Type:表示響應體的MIME類型; * Expires:表示文檔的過時時間,到期再也不緩存; * Last-Modified:表示文檔的最後改動時間; * Pragma:用來包含特定的指令; 可是當客戶端想要獲取額外的響應頭字段時,就須要服務端經過在該字段後定義相應的客戶端可獲取的響應頭字段名稱。


以上就是簡單跨域AJAX請求,客戶端與服務端的交互,在繼續介紹複雜的跨域AJAX請求前,讓咱們先停一停,回過頭來看看響應報頭的Access-Control-Allow-Origin字段,談一談CORS規範中爲何默認不容許跨域AJAX請求攜帶Cookie,以及若是客戶端須要傳送Cookie時,客戶端與服務端又該如何交互的問題。

首先,咱們要知道,在客戶端與服務端數據傳輸的過程當中,Cookie一直是以明文的形式伴隨着數據的傳輸,只要客戶端發送了Cookie至服務端,服務端就會至少返回該段Cookie。而咱們又提到過,大多數網站都使用Cookie短暫存儲用戶會話中的身份信息,所以將Cookie暴露在外是存在安全隱患的,CSRF攻擊的目的即是獲取用戶的Cookie信息,所以在跨域AJAX請求中,爲了減小Cookie泄露的風險,CORS規範默認禁止跨域AJAX請求攜帶Cookie。

那麼若是客戶端實在須要攜帶Cookie信息怎麼辦呢?正如上文提到過的,須要客戶端與服務端一塊兒配合,讓咱們看看具體細節:

  • 首先是客戶端:

開發者須要在建立XMLHttpRequest對象實例時,手動配置withCredentials屬性,將其值設置爲true

var xhr = new XMLHttpRequest()
xhr.withCredentials = true
複製代碼

某些瀏覽器會默認容許在跨域AJAX請求中發送Cookie,此時若是不想要發送Cookie,你只須要將其值設置爲false

  • 其次是服務端:

對於服務端而言,除了像以前提到的要在響應報頭設置Access-Control-Allow-Credential字段的值爲true以外,還須要爲Access-Control-Allow-Origin字段設置一個明確的域,不能夠再使用*號。

相信你也能明白,這一切都是爲了保護客戶端與服務端Cookie的隱私和安全。


如今咱們能夠繼續咱們的主題,一塊兒看一看若是咱們的跨域AJAX請求超出了「簡單」的標準,客戶端與服務端又應該如何相互配合,實現跨域的資源共享。

與簡單AJAX跨域請求不一樣,「複雜「的AJAX跨域請求一共會發送兩次HTTP請求,其中第一次爲」查詢請求「,第二次纔是咱們正式的」AJAX跨域請求「。爲何多出了一次」查詢請求「呢?道理其實很簡單,咱們想象一下當發送」複雜「的AJAX跨域請求時,瀏覽器最早拿到請求開始識別,而後發現這個請求並不「單純」(不知足簡單跨域AJAX請求標準),因而感到十分疑惑的瀏覽器會試探的沿着請求的地址向服務端發問,詢問服務端是否容許異域的客戶端向它發送額外的請求信息,這一次「發問」,便是第一次HTTP請求,即「查詢請求」。而服務端固然也會此次「發問」給出相應的回答,而後瀏覽器就會根據回答的結果決定是否繼續發送該跨域AJAX請求。

讓咱們看看具體的實現細節:

首先,讓咱們創造出一個「複雜」的AJAX跨域請求:

var url = 'http://another.com/cors'
var xhr = new XMLHttpRequest()
xhr.open('put', url, true) // 這裏咱們設置請求的方式爲'put'
xhr.setRequestHeader('X-Custom-Header', 'Value') // 這裏咱們自定義了一個請求頭字段
xhr.send()
複製代碼

當瀏覽器識別到該請求「並不簡單」時,就會自動向服務其發送一個「查詢請求」,其報頭信息大體以下:

OPTIONS /cors HTTP/1.1
Origin: http://thisOne.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: another.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
複製代碼

注意此次「查詢請求」使用了「OPTIONS」的請求方法,代表了這是一個查詢請求。請求頭部的信息說明了請求來源的域請求使用的HTTP方法以及請求額外發送的頭部字段

讓咱們再轉換至服務器視角,當服務端接收到瀏覽器發來的這樣一個查詢請求後,就能夠判斷出是否應該接收該請求。若是想要向瀏覽器表示容許該請求,則會返回這樣的響應報文:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61(Unix)
Access-Control-Allow-Origin: http://thisOne.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header // 該字段值爲以「,」號分割的字符串
Content-type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
複製代碼

讀到這裏咱們已經大概猜的出服務端向瀏覽器傳遞的信息了:

  • 首先,Access-Control-Allow-Origin字段向瀏覽器說明了發起AJAX請求的域是被服務器承認的(注意這個字段的值也能夠爲一個「*」號);
  • 其次,Access-Control-Allow-Methods字段向瀏覽器說明了服務器接收跨域AJAX的請求方式;
  • 最後,Access-Control-Allow-Headers字段向瀏覽器說明了服務器容許跨域AJAX額外發送的報頭信息;

當瀏覽器收到服務端這樣的表示贊成請求的響應後,就會正常發送接下來的跨域AJAX請求,而服務器也會正常的迴應。值的一提的是,在服務端與客戶端整個跨域AJAX請求的交互中,Access-Control-Allow-Origin頭信息自始至終都是必須攜帶的。

而當服務器在檢查「查詢請求」後,若是不一樣該請求,則會返回一個正常的HTTP響應,報文中包含任何與CORS規範有關的報頭字段,此時,瀏覽器就會心照不宣的明白服務器拒絕接收發出的跨域AJAX請求,所以會返回一個錯誤狀態(能夠被XML對象實例使用onerror回調函數捕獲)並在控制檯打印一條錯誤信息:

XMLHttpRequest cannot load http://another.com
Origin http://thisOne.com is not allowed by Access-Control-Allow-Origin
複製代碼

至此,不管是「簡單」的跨域AJAX請求仍是「複雜」的跨域AJAX請求,咱們都已經清楚的知曉了他們的運做原理,這真是件了不得的事情。可是先彆着急慶祝,咱們剛纔還遺漏了一個話題沒有談到:「節約複雜AJAX跨域請求的HTTP請求數」。

相信你還記的,對於「複雜」的跨域AJAX請求,瀏覽器會向服務器發送兩次HTTP請求,雖然實際上兩次HTTP請求與一次HTTP請求所耗費的時間幾乎難以感知,可是若是咱們有辦法一次搞定,又爲何還要重複作兩次呢?

對於服務器而言,「一次搞定」的方法就在於,在瀏覽器第一次發送複雜的跨域AJAX查詢請求時,在響應報頭中添加Access-Control-Max-Age字段,這是一個可選的字段,它用來指定本次查詢請求的有效期,單位爲秒。也就是說,經過該字段,服務器擁有了告知瀏覽器「這個請求我批准了,X秒之內不須要再向我確認」的能力。至此,咱們成功的將接下來的跨域請求數由兩次節約爲一次!

3、小結

一口氣看到這裏?真不容易! 但願這是值得的,讓咱們總結一下咱們在本文中都談到了些什麼。首先,咱們談到了咱們什麼時候須要發起跨域AJAX請求的問題,作到了「知其然」。其次,咱們深刻探討了使用JSONP技術和CORS規範實現發送跨域AJAX請求的細節,成功達到了咱們「知其因此然」的目標。相信如今的你已經對向他人談論「跨域」這個主題充滿自信。真的很棒對吧?

若是你依然以爲意猶未盡,不妨接着和我繼續深刻這個主題,看看實現跨域共享資源的另外兩種「時髦」的方式:使用 postMessage 和 webSocket。

感興趣嗎?休息一下,而後再回來,目前爲止你表現的都很是出色!🙌 。

👋 Hey!喜歡這篇文章嗎?別忘了在下方👇 點贊讓我知道。
相關文章
相關標籤/搜索