若是你正在開發一個現代的基於web的應用程序,那麼你:javascript
XMLHttpRequest cannot load http://external.service/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://my.app' is therefore not allowed access.java
每次我須要使用一些不能徹底控制的外部服務或一些服務器API集成web 應用程序,我就會碰到這個錯誤。Google尚未給我提供了關於這個問題一個簡潔的描述或替代執行跨域請求的概述,因此這篇文章將做爲未來我的參考。git
咱們之因此遇到這個問題,是由於咱們違反了同源策略(SOP)。這是在瀏覽器實行的一個安全措施,用來限制不一樣源之間的文檔(或腳本)的交互。github
一個網頁的同源是由其定義協議、域名和端口號決定的。例如,本頁面的源是(‘http’,‘jvaneyck.wordpress.com’,80).具備相同源的資源能夠徹底互相訪問。若是頁面A和頁面B擁有相同的源,那麼頁面A上的JavaScript能夠執行HTTP請求到頁面B的服務器,操縱頁面B的DOM,甚至讀取到頁面B的cookies。注意,源是有網頁的源地址定義的。闡明:從另外一個域加載的javascript源文件(例如,從遠程CDN引用的jQuery)將在HTML頁面的源中運行,這個HTML是包含scrip的頁面,而不是javascript文件來自的域。web
對於特定的跨域HTTP請求,SOP規定如下通常規則:容許跨域寫,禁止跨域讀取。這意味着若是A和C是不一樣的源,A發送的HTTP請求會由C正確地接收(這些就是「寫」),可是A中的腳本將沒法讀取任何數據--甚至來自C返回的響應代碼。這就是跨域「讀取」,並被瀏覽器屏蔽,致使出現上面的錯誤。換句話說,SOP不阻止攻擊者向他們源寫數據,它只是不容許他們讀取來自你的域的數據(cookie, localStorage 或其餘)或利用從他們域接收的響應來作任何事。編程
SOP是一件很是好的事情TM。它能夠防止惡意腳本讀取你的域的數據,並把其發送到他們的服務器。這意味着一些腳本小子將不能那麼輕易地竊取cookies。json
然而,有時你必須有意識地執行跨域請求。提示:這將須要一些額外的工做。合法的跨域請求示例:後端
取決於你在服務器端的控制數量,你有多個選項來啓用跨域請求。我將討論可能的解決方案:JSONP, 使用服務器端代理和CORS。跨域
還有其餘選擇,使用最普遍的的技術是使用iframes和window.postMessage。我不會再本文討論,可是對這些感興趣的能夠點擊這裏。瀏覽器
我寫了一些代碼來嘗試使用不一樣方法,這些均會在本文討論。你能夠在咱們的github中查看完整代碼。他們應該很容易在本地運行,若是你想本身嘗試的話。每一個例子有2個網站,一個網站在源(‘http’,'localhost',3000)另外一個在(‘http’,'localhost',3001)。他們是不一樣的源,因此3000請求3001被認爲是跨域請求並被瀏覽器默認屏蔽。
考慮如下場景:A域的頁面想要執行一個GET請求到域B的頁面。這就是所發生的:
瀏覽器把請求正確的發送到服務器:
GET / HTTP/1.1
服務器返回響應:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Content-Length: 57 { "response": "This is data returned from the server" }
然而在接收到響應,瀏覽器屏蔽響應進一步傳播,並顯示同源違反錯誤如上所示。例如,若是你使用jQuery,對GET請求進行done()回調將永遠不會執行,你將沒法讀取從服務器返回的數據。
JavaScript Object Notation with Padding(簡稱JSONP)是一種執行跨域請求的方法,經過利用HTML頁面的script 標籤能夠加載來自不一樣域的狀況。在咱們進入細節以前,我想說它又一些重大的問題:
JSONP依賴這樣的事實:<script>標籤能夠有來自不一樣域的資源。當瀏覽器解析<script>標籤,它會GET請求腳本內容(來自任何源)並在當前的頁面中執行。一般,服務器會返回HTML或一些顯示爲數據格式的數據如XML或JSON。然而當向一個啓用JSONP的服務器請求時,它會返回一個腳本塊,這個腳本塊執行一個回調函數,函數已在頁面中指定,提供的實際數據做爲參數。以防你的腦殼爆炸了,下面的示例會更具體。
源3000的頁面想要獲取存儲在源3001的資源。源3000頁面包含下面的script標籤:
<script
src='http://localhost:3001?callback=myCallbackFunction'>
</script>
當瀏覽器解析這個script標籤,它將正常的發出GET請求:
GET /?callback=myCallbackFunction HTTP/1.1
服務器無法返回原生JSON,而是返回一個腳本塊,包含一個對一個函數的調用,函數名在URL中指定,輸出的數據做爲參數傳遞。
HTTP/1.1 200 OK Content-Type: application/javascript myCallbackFunction({'response': 'hello world from JSONP!'});
這個腳本塊在瀏覽器接收到後就當即被執行。在腳本塊裏的函數調用是在當前頁面中評價的。當前頁面定義了回調函數,它使用返回的數據:
<script> function myCallbackFunction(data){ $('body').text(data.response); } </script>
總結:
另外一個繞過同源策略執行跨域請求是不作任何跨域請求的!若是你使用了一個代理,這個代理有你的域名,你能夠簡單地使用它在後端訪問外部服務,並把結果返回給你的客戶端代碼。由於請求代碼和代理在同一個域中,因此不違反同源策略。
這種技術不須要改變現有的服務器端代碼。它須要有服務器端代理服務,且在相同的域中一樣在瀏覽器中運行JavaScript代碼。
爲了完整性,我展現一個簡短例子:
不是直接向http://localhost:3001執行GET請求,咱們想本身域的代理服務器發送請求。
GET /proxy?urlToFetch=http%3A%2F%2Flocalhost%3A3001 HTTP/1.1
服務器將執行實際的GET請求外部服務。服務器端代碼能夠正常的執行跨域請求而不會發生錯誤,所以能夠成功的調用。代理服務將結果輸送給客戶:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 { "response": "This is data returned from the server, proxy style!" }
注意這種方法也有一些嚴重的缺點。例如,咱們尚未在這篇文章中涉及與安全相關的主題。若是第三方服務使用cookies進行身份嚴重,那麼你就不能使用這種方法。你本身的JavaScript代碼是不能訪問外部的域的cookies而且cookies也不能發送給你的代理服務,因此不能像第三方服務提供包含用戶憑據的cookies。
你可能正感受一個輕微的噁心。若是你以爲以前的機制都有「hacky」味道,那麼你是絕對正確的。前面的方法都是繞過合法的瀏覽器安全機制而且繞過它總有寫髒。
幸運地是,存在一個更乾淨的方法: Cross-Origin Resource Sharing(或者簡稱CORS).
CORS爲服務器提供了一個機制來告訴瀏覽器域A讀取請求自域B的數據是能夠的。它是經過在響應頭中包含一個新的Access-Control-Allow-Origin http 頭。若是你還記得引入的錯誤信息,這正是瀏覽器試圖告訴你的。當瀏覽器接收到跨域的響應時,它會檢查CORS 頭。若是響應頭中指定的源匹配當前源,它容許讀取訪問響應。不然,你會獲得討厭的錯誤信息。
一個具體的例子。
像往常同樣請求源3000:
GET / HTTP/1.1
源3001的服務器檢查是否這個源能夠訪問數據,並在響應中增長額外的Access-Control-Allow-Origin頭,列出請求源:
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000 Content-Type: application/json; charset=utf-8 Content-Length: 62 { "response": "This is data returned from the CORS server" }
當瀏覽器接收到響應時它比較請求源(3000)和列在Access-Control-Allow-Origin頭的源(也是3000)。因爲他們匹配,瀏覽器容許源3000的代解釋執行響應。
像以前同樣,這種方法有一些侷限性。例如IE比較老的版本只能部分支持CORS. 同時,對全部請求除了最簡單請求,你必須有雙倍的HTTP請求(參考:preflighting CORS requests)。
在這篇文章中,我試圖說明什麼類型的請求被分類爲跨域請求和在同源策略下他們爲何會被瀏覽器屏蔽。此外,我討論了幾種機制用來執行跨域請求。下表總結了這些機制。
機制 | 支持HTTP方法 | 服務器端修改要求 | 附註 |
JSONP | GET | Yes(返回script塊,包含函數調用替代元素JSON) | 須要徹底信任服務器 |
Proxy | ALL | No(可是你的源須要一個額外的代理組件) | 服務器後端執行請求,而不是瀏覽器。可能會產生進行身份驗證的問題 |
CORS | ALL | Yes(返回額外的HTTP 頭) | IE較老的版本不支持。更「複雜」的請求,須要額外的HTTP調用(preflighted 請求) |
正如你看到的,即便最簡單的跨域請求沒有銀彈。若是你已經控制服務器代碼且你不須要支持遺留的瀏覽器,我強烈建議你使用CORS方法。一如既往,評你當前的需求,使用最適合你的方法。
譯文:Cross-Domain requests in Javascript
不少地方翻譯的不對,歡迎指正。