一個資源會發起一個跨域HTTP請求(Cross-site HTTP request), 當它請求的一個資源是從一個與它自己提供的第一個資源的不一樣的域名時 。php
好比說,域名A(http://domaina.example)的某 Web 應用程序中經過<img>標籤引入了域名B(http://domainb.foo)站點的某圖片資源(http://domainb.foo/image.jpg),域名A的那 Web 應用就會致使瀏覽器發起一個跨站 HTTP 請求。在當今的 Web 開發中,使用跨站 HTTP 請求加載各種資源(包括CSS、圖片、JavaScript 腳本以及其它類資源),已經成爲了一種廣泛且流行的方式。html
正如你們所知,出於安全考慮,瀏覽器會限制腳本中發起的跨站請求。好比,使用 XMLHttpRequest
對象發起 HTTP 請求就必須遵照同源策略。 具體而言,Web 應用程序能且只能使用 XMLHttpRequest
對象向其加載的源域名發起 HTTP 請求,而不能向任何其它域名發起請求。爲了能開發出更強大、更豐富、更安全的Web應用程序,開發人員渴望着在不丟失安全的前提下,Web 應用技術能愈來愈強大、愈來愈豐富。好比,可使用 XMLHttpRequest
發起跨站 HTTP 請求。(這段描述跨域不許確,跨域並不是瀏覽器限制了發起跨站請求,而是跨站請求能夠正常發起,可是返回結果被瀏覽器攔截了。最好的例子是CSRF跨站攻擊原理,請求是發送到了後端服務器不管是否跨域!注意:有些瀏覽器不容許從HTTPS的域跨域訪問HTTP,好比Chrome和Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求,這是一個特例。)前端
隸屬於 W3C 的 Web 應用工做組( Web Applications Working Group )推薦了一種新的機制,即跨源資源共享(Cross-Origin Resource Sharing (CORS))。這種機制讓Web應用服務器能支持跨站訪問控制,從而使得安全地進行跨站數據傳輸成爲可能。須要特別注意的是,這個規範是針對API容器的。好比說,要使得 XMLHttpRequest
在現代瀏覽器中能夠發起跨域請求。瀏覽器必須能支持跨源共享帶來的新的組件,包括請求頭和策略執行。一樣,服務器端則須要解析這些新的請求頭,並按照策略返回相應的響應頭以及所請求的資源。這篇文章適用於網站管理員、服務器端程序開發人員以及前端開發人員。對於服務器端程序開發人員,還能夠閱讀補充材料 cross-origin sharing from a server perspective (with PHP code snippets) 。html5
跨源資源共享標準( cross-origin sharing standard ) 使得如下場景可使用跨站 HTTP 請求:web
XMLHttpRequest
發起跨站 HTTP 請求。 @font-face
使用跨站字體資源), 所以,網站就能夠發佈 TrueType 字體資源,並只容許已受權網站進行跨站調用。接下來的文章,會對跨源資源共享作一個總覽,並介紹下在 Firefox 3.5 中已實現的跨源資源共享所使用的 HTTP 頭。shell
跨源資源共享標準經過新增一系列 HTTP 頭,讓服務器能聲明哪些來源能夠經過瀏覽器訪問該服務器上的資源。另外,對那些會對服務器數據形成破壞性影響的 HTTP 請求方法(特別是 GET 之外的 HTTP 方法,或者搭配某些MIME類型的POST請求),標準強烈要求瀏覽器必須先以 OPTIONS
請求方式發送一個預請求(preflight request),從而獲知服務器端對跨源請求所支持 HTTP 方法。在確認服務器容許該跨源請求的狀況下,以實際的 HTTP 請求方法發送那個真正的請求。服務器端也能夠通知客戶端,是否是須要隨同請求一塊兒發送信用信息(包括 Cookies 和 HTTP 認證相關數據)。canvas
隨後的章節,將對相關情景及用到的 HTTP 請求進行討論。後端
在此,咱們會用三個場景來解釋跨源共享是怎麼運行的。其中,全部的跨站請求都是經過 XMLHttpRequest
對象發起。跨域
若是對如下章節中的 JavaScript 代碼片斷感興趣,能夠訪問這兒。在全部支持跨站 XMLHttpRequest 請求的瀏覽中,能夠看到實際運行效果。而若是想繼續瞭解服務器端對跨源請求的處理,則能夠訪問這兒。
瀏覽器
所謂的簡單,是指:
application/x-www-form-urlencoded
, multipart/form-data 或 text/plain
中的一種。好比說,假如站點 http://foo.example 的網頁應用想要訪問 http://bar.other 的資源。如下的 JavaScript 代碼應該會在 foo.example 上執行:
var invocation = new XMLHttpRequest(); var url = 'http://bar.other/resources/public-data/'; function callOtherDomain() { if(invocation) { invocation.open('GET', url, true); invocation.onreadystatechange = handler; invocation.send(); } }
讓咱們看看,在這個場景中,瀏覽器會發送什麼的請求到服務器,而服務器又會返回什麼給瀏覽器:
GET /resources/public-data/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Referer: http://foo.example/examples/access-control/simpleXSInvocation.html Origin: http://foo.example HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 00:23:53 GMT Server: Apache/2.0.61 Access-Control-Allow-Origin: * Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: application/xml [XML Data]
第 1~10 行是 Firefox 3.5 發出的請求頭。注意看第10行的請求頭 Origin,它代表了該請求來自於 http://foo.exmaple。
第 13~22 行則是 http://bar.other 服務器的響應。如第16行所示,服務器返回了響應頭 Access-Control-Allow-Origin: *,這代表服務器接受來自任何站點的跨站請求。若是服務器端僅容許來自 http://foo.example 的跨站請求,它能夠返回:
Access-Control-Allow-Origin: http://foo.example
如今,除了 http://foo.example,其它站點就不能跨站訪問 http://bar.other 的資源了。
如上,經過使用 Origin 和 Access-Control-Allow-Origin 就能夠完成最簡單的跨站請求。不過 Access-Control-Allow-Origin 須要爲 * 或者包含由 Origin 指明的站點。
不一樣於上面討論的簡單請求,「預請求」要求必須先發送一個 OPTIONS 請求給目的站點,來查明這個跨站請求對於目的站點是否是安全可接受的。這樣作,是由於跨站請求可能會對目的站點的數據形成破壞。 當請求具有如下條件,就會被當成預請求處理:
application/x-www-form-urlencoded, multipart/form-data 或者
text/plain
之外的數據類型。好比說,用 POST 發送數據類型爲 application/xml 或者 text/xml 的 XML 數據的請求。
Note: 從Gecko 2.0開始,text/plain,
application/x-www-form-urlencoded 和 multipart/form-data
類型的數據均可以直接用於跨站請求,而不須要先發起「預請求」了。以前,只有 text/plain
能夠不用先發起「預請求」,進行跨站請求。
示例以下:
var invocation = new XMLHttpRequest(); var url = 'http://bar.other/resources/post-here/'; var body = '{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}Arun'; function callOtherDomain(){ if(invocation) { invocation.open('POST', url, true); invocation.setRequestHeader('X-PINGOTHER', 'pingpong'); invocation.setRequestHeader('Content-Type', 'application/xml'); invocation.onreadystatechange = handler; invocation.send(body); } ......
如上,以 XMLHttpRequest 建立了一個 POST 請求,爲該請求添加了一個自定義請求頭(X-PINGOTHER: pingpong),並指定數據類型爲 application/xml。因此,該請求是一個「預請求」形式的跨站請求。
讓咱們看看服務器與瀏覽器之間具體的交互過程:
OPTIONS /resources/post-here/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Origin: http://foo.example Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER 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://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER Access-Control-Max-Age: 1728000 Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain POST /resources/post-here/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive X-PINGOTHER: pingpong Content-Type: text/xml; charset=UTF-8 Referer: http://foo.example/examples/preflightInvocation.html Content-Length: 55 Origin: http://foo.example Pragma: no-cache Cache-Control: no-cache Arun HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:40 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://foo.example Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 235 Keep-Alive: timeout=2, max=99 Connection: Keep-Alive Content-Type: text/plain [Some GZIP'd payload]
第1至12行,使用一個 OPTIONS 發送了一個「預請求」。
Firefox 3.1 根據請求參數,決定須要發送一個「預請求」,來探明服務器端是否接受後續真正的請求。 OPTIONS 是 HTTP/1.1 裏的方法,用來獲取更多服務器端的信息,是一個不該該對服務器數據形成影響的方法。 隨同 OPTIONS 請求,如下兩個請求頭一塊兒被髮送:
Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER
請求頭Access-Control-Request-Method
能夠提醒服務器跨站請求將使用POST方法,而
Access-Control-Request-Headers則告知服務器該跨站請求將攜帶一個自定義請求頭請求頭
X-PINGOTHER。這樣,服務器就能夠決定,在當前狀況下,是否接受該跨站請求訪問。
第15至27行是服務器的響應。該響應代表,服務器接受了客服端的跨站請求。具體能夠看看第18至21行:
Access-Control-Allow-Origin: http://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER Access-Control-Max-Age: 1728000
響應頭Access-Control-Allow-Methods代表服務器能夠接受
POST
, GET和
OPTIONS
的請求方法。請注意,這個響應頭相似於HTTP/1.1 Allow: response header,但僅限於訪問控制的場景下。而響應頭Access-Control-Allow-Headers則表示服務器接受自定義請求頭
就像X-PINGOTHER。
Access-Control-Allow-Methods同樣,
Access-Control-Allow-Headers容許以逗號分隔,傳遞一個可接受的自定義請求頭列表。最後,
響應頭Access-Control-Max-Age告訴瀏覽器
,本次「預請求」的響應結果有效時間是多久。在上面的例子裏,1728000秒錶明着20天內,瀏覽器在處理針對該服務器的跨站請求,均可以無需再發送「預請求」,只需根據本次結果進行判斷處理。
XMLHttpRequest
和訪問控制功能,最有趣的特性就是,發送憑證請求(HTTP Cookies和驗證信息)的功能。通常而言,對於跨站請求,瀏覽器是不會發送憑證信息的。但若是將XMLHttpRequest
的一個特殊標誌位設置爲true,瀏覽器就將容許該請求的發送。
http://foo.example站點的腳本向
值。腳本代碼以下:http://bar.other站點發送一個GET請求,並設置了一個Cookies
var invocation = new XMLHttpRequest(); var url = 'http://bar.other/resources/credentialed-content/'; function callOtherDomain(){ if(invocation) { invocation.open('GET', url, true); invocation.withCredentials = true; invocation.onreadystatechange = handler; invocation.send(); }
如你所見,第七行代碼將XMLHttpRequest
的withCredentials標誌設置爲true,
從而使得Cookies能夠隨着請求發送。由於這是一個簡單的GET請求,因此瀏覽器不會發送一個「預請求」。可是,若是服務器端的響應中,若是沒有返回Access-Control-Allow-Credentials: true的響應頭,那麼瀏覽器將不會把響應結果傳遞給發出請求的腳本程序,以保證信息的安全。
客服端與服務器端交互示例以下:
GET /resources/access-control-with-credentials/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Referer: http://foo.example/examples/credential.html Origin: http://foo.example Cookie: pageAccess=2 HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:34:52 GMT Server: Apache/2.0.61 (Unix) PHP/4.4.7 mod_ssl/2.0.61 OpenSSL/0.9.7e mod_fastcgi/2.4.2 DAV/2 SVN/1.4.2 X-Powered-By: PHP/5.2.6 Access-Control-Allow-Origin: http://foo.example Access-Control-Allow-Credentials: true Cache-Control: no-cache Pragma: no-cache Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 106 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain [text/plain payload]
雖然第11行指定了要提交到http://bar.other的內容的Cookie信息,可是若是bar.other的響應頭裏沒有Access-Control-Allow-Credentials:true(第19行),則響應會被忽略. 特別注意: 給一個帶有withCredentials的請求發送響應的時候,服務器端必須指定容許請求的域名,不能使用'*'.上面這個例子中,若是響應頭是這樣的:Access-Control-Allow-Origin: * ,則響應會失敗. 在這個例子裏,由於
Access-Control-Allow-Origin的值是http://foo.example這個指定的請求域名,因此客戶端把帶有憑證信息的內容被返回給了客戶端. 另外注意第22行,更多的cookie信息也被建立了.
上面這些例子的運行能夠查看這裏.下一部分將討論實際的HTTP頭信息.
這部分裏列出了跨域資源共享(Cross-Origin Resource Sharing)時,服務器端須要返回的響應頭信息.上一部份內容是這部份內容在實際運用中的一個概述.
返回的資源須要有一個 Access-Control-Allow-Origin 頭信息,語法以下:
Access-Control-Allow-Origin: <origin> | *
origin參數指定一個容許向該服務器提交請求的URI.對於一個不帶有credentials的請求,能夠指定爲'*',表示容許來自全部域的請求.
舉個栗子,容許來自 http://mozilla.com 的請求,你能夠這樣指定:
Access-Control-Allow-Origin: http://mozilla.com
若是服務器端指定了域名,而不是'*',那麼響應頭的Vary值裏必須包含Origin.它告訴客戶端: 響應是根據請求頭裏的Origin的值來返回不一樣的內容的.
設置瀏覽器容許訪問的服務器的頭信息的白名單:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
這樣, X-My-Custom-Header
和 X-Another-Custom-Header這兩個頭信息,均可以被瀏覽器獲得.
這個頭告訴咱們此次預請求的結果的有效期是多久,以下:
Access-Control-Max-Age: <delta-seconds>
delta-seconds
參數表示,容許這個預請求的參數緩存的秒數,在此期間,不用發出另外一條預檢請求.
告知客戶端,當請求的credientials屬性是true的時候,響應是否能夠被獲得.當它做爲預請求的響應的一部分時,它用來告知實際的請求是否使用了credentials.注意,簡單的GET請求不會預檢,因此若是一個請求是爲了獲得一個帶有credentials的資源,而響應裏又沒有Access-Control-Allow-Credentials頭信息,那麼說明這個響應被忽略了.
Access-Control-Allow-Credentials: true | false
帶有credential的請求在上面討論.
指明資源能夠被請求的方式有哪些(一個或者多個). 這個響應頭信息在客戶端發出預檢請求的時候會被返回. 上面有相關的例子.
Access-Control-Allow-Methods: <method>[, <method>]*
發出預檢請求的例子見上,這個例子裏就有向客戶端發送Access-Control-Allow-Methods響應頭信息.
也是在響應預檢請求的時候使用.用來指明在實際的請求中,可使用哪些自定義HTTP請求頭.好比
Access-Control-Allow-Headers: X-PINGOTHER
這樣在實際的請求裏,請求頭信息裏就能夠有這麼一條:
X-PINGOTHER: pingpong
能夠有多個自定義HTTP請求頭,用逗號分隔.
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
這部份內容列出來當瀏覽器發出跨域請求時可能用到的HTTP請求頭.注意這些請求頭信息都是在請求服務器的時候已經爲你設置好的,當開發者使用跨域的XMLHttpRequest的時候,不須要手動的設置這些頭信息.
代表發送請求或者預請求的域
Origin: <origin>
參數origin是一個URI,告訴服務器端,請求來自哪裏.它不包含任何路徑信息,只是服務器名.
注意,不只僅是跨域請求,普通請求也會帶有ORIGIN頭信息.
在發出預檢請求時帶有這個頭信息,告訴服務器在實際請求時會使用的請求方式
Access-Control-Request-Method: <method>
相關的例子能夠在這裏找到
在發出預檢請求時帶有這個頭信息,告訴服務器在實際請求時會攜帶的自定義頭信息.若有多個,能夠用逗號分開.
Access-Control-Request-Headers: <field-name>[, <field-name>]*
相關的例子能夠在這裏找到
Feature | Chrome | Firefox (Gecko) | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
Basic support | 4 | 3.5 | 8 (via XDomainRequest) 10 |
12 | 4 |
Internet Explorer 8 和 9 經過 XDomainRequest 對象來實現CORS ,可是在IE 10中有完整的實現。Firefox 3.5 就引入了對跨站 XMLHttpRequests 和 Web 字體的支持 ,儘管存在着一些直到後續版本才取消的限制。特別的, Firefox 7 引入了對跨站 WebGL 紋理的 HTTP 請求的支持,並且 Firefox 9 添加對經過 drawImage 在 canvas 上繪圖的支持。
XMLHttpRequest
and Cross-Origin Resource SharingXMLHttpRequest