現在請跟我做:在您的瀏覽器的地址欄中輸入www.yhd.com並敲擊回車。在網站內容全部加載完畢后,按F12打開瀏覽器的調試窗口。當切換到Sources頁時,您會發現您當前所看到的一號店的頁面是從多個不同的域中得到的:
或許有些讀者會感到奇怪:在之前自己 寫網頁的時候就曾經嘗試訪問非當前域中的資源,卻怎么也不成功,一號店是如何做到的?
當然,這不是一號店的獨門絕技,而僅僅是使用了一些跨域訪問的技術而已。而在本文中,我們就將對一種跨域訪問技術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請求。在得知了請求的類型后,我們就可以決定到底以哪種方式響應用戶了。這里的邏輯較為簡單,因此我就不再贅述了。
轉載請注明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/4592246.html
商業轉載請事先與我聯系:silverfox715@sina.com