跨域解決方案CORS簡介
現在請跟我做:在您的瀏覽器的位址列中輸入www.yhd.com並敲擊回車。在網站內容全部載入完畢後,按F12開啟瀏覽器的除錯視窗。當切換到Sources頁時,您會發現您當前所看到的一號店的頁面是從多個不同的域中得到的:
或許有些讀者會感到奇怪:在之前自己 寫網頁的時候就曾經嘗試訪問非當前域中的資源,卻怎麼也不成功,一號店是如何做到的?
其實,這裡用到的是一種叫js跨區訪問的技術:這裡說的js跨域是指通過js在不同的域之間進行資料傳輸或通訊,比如用ajax向一個不同的域請求資料,或者通過js獲取頁面中不同域的框架中(iframe)的資料。只要協議、域名、埠有任何一個不同,都被當作是不同的域
在本文中,我們就將對一種跨域訪問技術CORS(Cross-Origin Resource Sharing)進行介紹。
為什麼要用CORS
在需要做出一個技術決定時,我們常常需要給出適當的理由。就CORS而言,使用它的根本原因就是要完成資源的跨域訪問,也就是如何繞過Same-origin Policy。
那麼什麼是Same-origin Policy呢?簡單地說,在一個瀏覽器中訪問的網站不能訪問另一個網站中的資料,除非這兩個網站具有相同的Origin,也即是擁有相同的協議、主機地址以及埠。一旦這三項資料中有一項不同,那麼該資源就將被認為是從不同的Origin得來的,進而不被允許訪問。
但是這個限制的確過於嚴格了:一個大型網站常常擁有一系列子域。在這些域之間交換資料就會受到Same-origin Policy的限制。為了繞過該限制,業界提出了一系列解決該問題的方法,例如更改document.domain屬性,跨文件訊息,JSONP以及CORS等。這些解決方案各有各的長處,因此我們需要根據需求的不同來對這些方案進行選擇。
可以說更改document.domain屬性的方法是最為直接快速的的方法,也較為常見。通過將從不同域中得到的指令碼的document.domain屬性設定為同一個值,就可以使得這些指令碼之間可以相互互動。例如從“http://blog.ambergarden.com”得到的網頁可以通過執行如下的指令碼改變其document.domain屬性中記錄的所屬域:
1 document.domain = ‘ambergarden.com’;
那麼接下來,該指令碼就可以訪問ambergarden.com中的資料了。
這種方法也有其自身的劣勢,那就是軟體開發人員不可以隨便設定document.domain屬性的值,至少在一些瀏覽器上是如此的。
跨文件訊息則是通過向Window例項傳送訊息來完成的。在使用時,軟體開發人員需要通過呼叫一個Window的postMessage()函式來向該Window例項傳送訊息。此時Window例項內部的onmessage事件將被觸發,進而使得該事件的訊息處理函式被呼叫。但是在接收到訊息的時候,訊息處理函式首先需要判斷訊息來源的合法性,以避免惡意使用者通過傳送訊息的方式來非法執行程式碼。
JSONP則是通過在文件中嵌入一個<script>標記來從另一個域中返回資料。例如在頁面中新增一個如下的<script>標記:
1 <script src="http://blog.ambergarden.com/someData?callback=some_func"/>
該<script>標記會向http://blog.ambergarden.com/someData傳送一個GET請求。在資料返回到客戶端後,some_func()函式將會被呼叫。當然,這種方法擁有一個顯著的缺點,那就是隻支援GET操作。
就如您剛剛看到的一樣,上面所列出的各個方法各自有各自的缺點及侷限性。而相較於這些方法,CORS則沒有那麼多工作需要去做,也沒有那麼多限制。因此在本文中,我們將主要對CORS進行講解。
CORS執行流程
現在我們就來看一個通過CORS來進行跨域訪問的簡單示例。假設ambergarden.com想從一個公有資料平臺public-data.com中返回一些資料,那麼在頁面邏輯中,其可以通過下面的程式碼向public-data.com傳送資料請求:
1 function retrieveData() { 2 var request = new XMLHttpRequest(); 3 request.open('GET', 'http://public-data.com/someData', true); 4 request.onreadystatechange = handler; 5 request.send(); 6 }
在執行這段程式碼的之後,瀏覽器會向服務傳送如下的請求:
1 GET /someData/ HTTP/1.1 2 Host: public-data.com 3 ...... 4 Referer: http://ambergarden.com/somePage.html 5 Origin: http://ambergarden.com
而一個支援CORS協議的服務可能會給出下面的響應:
1 HTTP/1.1 200 OK 2 Access-Control-Allow-Origin: http://ambergarden.com 3 Content-Type: application/xml 4 ...... 5 6 [Payload Here]
這裡有一個值得注意的響應頭:Access-Control-Allow-Origin。該響應頭用來記錄可以訪問該資源的域。在接收到服務端響應後,瀏覽器將會檢視響應中是否包含Access-Control-Allow-Origin響應頭。如果該響應頭存在,那麼瀏覽器會分析該響應頭中所標示的內容。如果其包含了當前頁面所在的域,那麼瀏覽器就將知道這是一個被允許的跨域訪問,從而不再根據Same-origin Policy來限制使用者對該資料的訪問。
從整個訪問資料的流程來看,使用者所使用的跨域訪問資料的指令碼實際上和普通的訪問同一個域中資料的指令碼並沒有什麼不同。而不同的,僅僅是在響應中多了一個Access-Control-Allow-Origin響應頭。
是不是很簡單?實際上我們展示的僅僅是最為簡單的Simple Request的執行流程。而CORS則將導致跨域訪問的請求分為三種:Simple Request,Preflighted Request以及Requests with Credential。
如果一個請求沒有包含任何自定義請求頭,而且它所使用HTTP動詞是GET,HEAD或POST之一,那麼它就是一個Simple Request。但是在使用POST作為請求的動詞時,該請求的Content-Type需要是application/x-www-form-urlencoded,multipart/form-data或text/plain之一。
如果一個請求包含了任何自定義請求頭,或者它所使用的HTTP動詞是GET,HEAD或POST之外的任何一個動詞,那麼它就是一個Preflighted Request。如果POST請求的Content-Type並不是application/x-www-form-urlencoded,multipart/form-data或text/plain之一,那麼其也是Preflighted Request。
一般情況下,一個跨域請求不會包含當前頁面的使用者憑證。一旦一個跨域請求包含了當前頁面的使用者憑證,那麼其就屬於Requests with Credential。
前面我們已經看過瀏覽器對Simple Request是如何進行處理的。那麼接下來我們就來看看Preflight Request是如何執行的。相較於Simple Request,Preflight Request的執行流程則略為複雜一些。
假設現在我們要向公有資料平臺public-data.com寫入一些資料,那麼我們就需要傳送一個POST請求:
1 function sendData() { 2 var request = new XMLHttpRequest(), 3 payload = ......; 4 request.open('POST', 'http://public-data.com/someData', true); 5 request.setRequestHeader('X-CUSTOM-HEADER', 'custom_header_value'); 6 request.onreadystatechange = handler; 7 request.send(payload); 8 }
在執行了該段程式碼之後,瀏覽器首先發出的請求將如下所示:
1 OPTIONS /someData/ HTTP/1.1 2 Host: public-data.com 3 ...... 4 Origin: http://ambergarden.com 5 Access-Control-Request-Method: POST 6 Access-Control-Request-Headers: X-CUSTOM-HEADER
可以看到,我們首先發送的並不是POST請求,而是OPTION請求。該請求還通過Access-Control-Request-Method以及Access-Control-Request-Headers標示了請求型別以及請求中所包含的自定義HTTP Header。實際上,它相當於向服務端詢問訪問資源的許可權:“您好,我想向你這裡傳送資料,你看可以嗎?”。而在真正訪問資源前傳送一個請求進行探測也是該請求被稱為是Preflight Request的原因。
在服務端看到該OPTIONS請求後,其將分析該請求中的內容並返回一個響應,以通知瀏覽器是否允許向它傳送資料:
1 HTTP/1.1 200 OK 2 Access-Control-Allow-Origin: http://ambergarden.com 3 Access-Control-Allow-Methods: POST, GET, OPTIONS 4 Access-Control-Allow-Headers: X-CUSTOM_HEADER 5 Access-Control-Max-Age: 1728000 6 ......
瀏覽器分析該響應並瞭解到其被允許向服務端傳送資料以後,其才會向服務端傳送真正的POST請求:
1 POST /someData/ HTTP/1.1 2 Host: public-data.com 3 X-CUSTOM-HEADER: custom_header_value 4 ...... 5 6 [Payload Here]
而服務端則會接收並處理該請求:
1 HTTP/1.1 200 OK 2 Access-Control-Allow-Origin: http://ambergarden.com 3 Content-Type: application/xml 4 ...... 5 6 [Payload Here]
最後一種請求Requests with Credential的執行流程則和前兩種請求類似。只不過在傳送請求的時候,我們需要將使用者憑證包含在請求中:
1 function retrieveData() { 2 var request = new XMLHttpRequest(); 3 request.open('GET', 'http://public-data.com/someData', true); 4 request.withCredentials = true; 5 request.onreadystatechange = handler; 6 request.send(); 7 }
而在服務端的響應中,其將擁有一個額外的Access-Control-Allow-Credentials響應頭:
1 HTTP/1.1 200 OK 2 Access-Control-Allow-Origin: http://ambergarden.com 3 Content-Type: application/xml 4 ...... 5 6 [Payload Here]
整合對CORS的支援
從上面的示例中已經能夠看到,在使用CORS來訪問資料的時候,客戶端不需要更改任何資料訪問邏輯。所有的一切工作都是在服務端及瀏覽器之間自動完成的。因此如果希望為一個系統整合CORS支援的時候,我們需要做的工作主要集中在服務端。
當然,整合工作實際上十分簡單:在你的web.xml中新增一個Filter(或利用已有的Filter)並根據傳入的請求首先判斷其是哪一種CORS請求。在得知了請求的型別後,我們就可以決定到底以哪種方式響應使用者了。這裡的邏輯較為簡單,因此我就不再贅述了。