在您的瀏覽器的地址欄中輸入www.yhd.com並敲擊回車。在網站內容所有加載完畢後,按F12打開瀏覽器的調試窗口。當切換到Sources頁時,您會發現您當前所看到的一號店的頁面是從多個不一樣的域中獲得的:web
或許有些讀者會感到奇怪:在以前本身 寫網頁的時候就曾經嘗試訪問非當前域中的資源,卻怎麼也不成功,一號店是如何作到的?跨域
固然,這不是一號店的獨門絕技,而僅僅是使用了一些跨域訪問的技術而已。而在本文中,咱們就將對一種跨域訪問技術CORS(Cross-Origin Resource Sharing)進行介紹。瀏覽器
爲何要用CORSapp
在須要作出一個技術決定時,咱們經常須要給出適當的理由。就CORS而言,使用它的根本緣由就是要完成資源的跨域訪問,也就是如何繞過Same-origin Policy。dom
那麼什麼是Same-origin Policy呢?簡單地說,在一個瀏覽器中訪問的網站不能訪問另外一個網站中的數據,除非這兩個網站具備相同的Origin,也便是擁有相同的協議、主機地址以及端口。一旦這三項數據中有一項不一樣,那麼該資源就將被認爲是從不一樣的Origin得來的,進而不被容許訪問。函數
可是這個限制的確過於嚴格了:一個大型網站經常擁有一系列子域。在這些域之間交換數據就會受到Same-origin Policy的限制。爲了繞過該限制,業界提出了一系列解決該問題的方法,例如更改document.domain屬性,跨文檔消息,JSONP以及CORS等。這些解決方案各有各的長處,所以咱們須要根據需求的不一樣來對這些方案進行選擇。post
能夠說更改document.domain屬性的方法是最爲直接快速的的方法,也較爲常見。經過將從不一樣域中獲得的腳本的document.domain屬性設置爲同一個值,就可使得這些腳本之間能夠相互交互。例如從「http://blog.ambergarden.com」獲得的網頁能夠經過執行以下的腳本改變其document.domain屬性中記錄的所屬域:網站
1 document.domain = ‘ambergarden.com’;
那麼接下來,該腳本就能夠訪問ambergarden.com中的數據了。this
這種方法也有其自身的劣勢,那就是軟件開發人員不能夠隨便設置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請求。在得知了請求的類型後,咱們就能夠決定到底以哪一種方式響應用戶了。