https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
當一個資源請求一個其它域名或者另外一個端口的資源時會產生一個跨域HTTP請求(cross-origin HTTP request)。比如說,http://domaina.example的某HTML頁面通過 <img> 的src 請求 http://domainb.foo/image.jpg。在當今的 Web 開發中,許多頁面都會從另外一個站點加載各類資源(包括CSS、圖片、JavaScript 腳本以及其它類資源)。
出於安全考慮,瀏覽器會限制腳本中發起的跨域請求。比如,使用 XMLHttpRequest
和 Fetch 發起 HTTP 請求就必須遵守同源策略。所以,Web 應用程序通過 XMLHttpRequest
對象或Fetch能且只能向同域名的資源發起 HTTP 請求,而不能向任何其它域名發起請求。 為了改進/提升Web應用程序,開發人員要求瀏覽器供應商允許跨域請求。 (譯者注:這段描述跨域不准確,跨域並非瀏覽器限制了發起跨站請求,而是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。最好的例子是CSRF跨站攻擊原理,請求是發送到了后端服務器無論是否跨域!注意:有些瀏覽器不允許從HTTPS的域跨域訪問HTTP,比如Chrome和Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求,這是一個特例。)
跨源資源共享 ( CORS )機制讓Web應用服務器能支持跨站訪問控制,從而使得安全地進行跨站數據傳輸成為可能。需要特別注意的是,這個規范是針對API容器的(比如說 XMLHttpRequest
或者 Fetch ),以減輕跨域HTTP請求的風險。
這篇文章適用於網站管理員、服務器端程序開發人員以及前端開發人員。現代瀏覽器支持跨源共享的客戶端組件,包括請求頭和策略執行。同樣,服務器端則需要解析這些新的請求頭,並按照策略返回相應的響應頭以及所請求的資源。對於服務器端程序開發人員,還可以閱讀補充材料 cross-origin sharing from a server perspective (with PHP code snippets) 。
跨源資源共享標准( cross-origin sharing standard ) 使得以下場景可以使用跨站 HTTP 請求:
- 如上所述,使用
XMLHttpRequest
或 Fetch發起跨站 HTTP 請求。 - Web 字體 (CSS 中通過
@font-face
使用跨站字體資源), 因此,網站就可以發布 TrueType 字體資源,並只允許已授權網站進行跨站調用。 - WebGL 貼圖
- 使用
drawImage
將 Images/video 畫面繪制到canvas. - 樣式表(使用 CSSOM)
- Scripts (未處理的異常)
接下來的文章,會對跨源資源共享做一個總覽,並介紹下需要使用的 HTTP 頭。
概述EDIT
跨源資源共享標准通過新增一系列 HTTP 頭,讓服務器能聲明哪些來源可以通過瀏覽器訪問該服務器上的資源。另外,對那些會對服務器數據造成副作用的 HTTP 請求方法(特別是 GET
以外的 HTTP 方法,或者搭配某些MIME類型的 POST
請求),標准強烈要求瀏覽器必須先以
OPTIONS
請求方式發送一個預請求(preflight request),從而獲知服務器端對跨源請求所支持 HTTP 方法。在確認服務器允許該跨源請求的情況下,以實際的 HTTP 請求方法發送那個真正的請求。服務器端也可以通知客戶端,是不是需要隨同請求一起發送信用信息(包括 Cookies 和 HTTP 認證相關數據)。
隨后的章節,將對相關情景及用到的 HTTP 請求進行討論。
一些訪問控制場景EDIT
在此,我們會用三個場景來解釋跨源共享是怎么運行的。其中,所有例子使用的都是能發起跨站請求XMLHttpRequest
對象。
如果對以下章節中的 JavaScript 代碼片段感興趣,可以訪問 http://arunranga.com/examples/access-control/。在所有支持跨站 XMLHttpRequest
請求的瀏覽中,可以看到實際運行效果。
而如果想繼續了解服務器端對跨源請求的處理,則可以訪問Server-Side_Access_Control(CORS)。
簡單請求
一些請求不會觸發 CORS preflight。而這部分在本文中被稱為“簡單請求”,雖然 Fetch (定義 CORS的)不使用這個術語。滿足下述條件的就是“簡單請求”:
- 只允許下列方法:
- 除了用戶代理自動設置的頭部外(比如
Connection
,User-Agent
,或者其他任意的 Fetch 規范定義的 “禁止的頭部名” ),唯一允許人工設置的頭部是 Fetch 規范定義的“ CORS-safelisted request-header”,如下:Accept
Accept-Language
Content-Language
Content-Type
(but note the additional requirements below)DPR
Downlink
Save-Data
Viewport-Width
Width
- 允許的
Content-Type
值有:application/x-www-form-urlencoded
multipart/form-data
text/plain
Accept
,
Accept-Language
, and
Content-Language
頭部的值。如果這些頭部的值是非標准的,WebKit/Safari 就不會將這些請求視為“簡單請求”。WebKit/Safari 並沒有將非標准的頭部值列個文檔出來,但是可以在 WebKit bug 里體現:
Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language,
Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS, and
Switch to a blacklist model for restricted Accept headers in simple CORS requests。其它的瀏覽器並沒有這個限制的實現,因為這個不在規范的內容里。
比如說,假如站點 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(); } }
這將導致客戶端和服務器之間發生簡單交換,使用CORS頭來處理跨域:
讓我們看看,在這個場景中,瀏覽器會發送什么的請求到服務器,而服務器又會返回什么給瀏覽器:
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 行是發出的請求頭。注意看第10行的請求頭 Origin
,它表明了該請求來自於http://foo.exmaple
。
第 13~22 行則是 http://bar.other 服務器的響應。如第16行所示,服務器返回了響應頭 Access-Control-Allow-Origin
。使用 Origin 和 Access-Control-Allow-Origin 就能完成最簡單的訪問控制。本例中,服務器返回了 Access-Control-Allow-Origin: *
,這意味着該資源跨域在跨域行為里可以被任意站點訪問。如果服務器端僅允許來自 http://foo.example 的跨站請求,它可以返回:
Access-Control-Allow-Origin: http://foo.example
現在,除了 http://foo.example,其它站點就不能跨站訪問 http://bar.other 的資源了(在請求頭里 ORIGIN 中定義,見上述第10行)。Access-Control-Allow-Origin
需要為 * 或者包含由 Origin 指明的站點。
預請求
不同於上面討論的簡單請求,“預請求”要求必須先發送一個 OPTIONS
方法請求給目的站點,來查明這個跨站請求對於目的站點是不是安全的可接受的。這樣做,是因為跨站請求可能會對目的站點的數據產生影響。 當請求具備以下條件,就會被當成預請求處理:
- 使用下述方法以外的請求:
- 除了用戶代理自動設置的頭部外(比如
Connection
,User-Agent
,或者其他任意的 Fetch 規范定義的 “禁止的頭部名” ),預請求不包括 Fetch 規范定義的“CORS-safelisted request-header”:Accept
Accept-Language
Content-Language
Content-Type
(but note the additional requirements below)DPR
Downlink
Save-Data
Viewport-Width
Width
-
Content-Type
頭部的值除了下列之外的:application/x-www-form-urlencoded
multipart/form-data
text/plain
注意: WebKit Nightly 和 Safari Technology Preview 添加了額外的限制在 Accept
, Accept-Language
, and Content-Language
頭部的值。如果這些頭部的值是非標准的,WebKit/Safari 就不會將這些請求視為“簡單請求”。WebKit/Safari 並沒有將非標准的頭部值列個文檔出來,但是可以在 WebKit bug 里體現:Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language, Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS, and Switch to a blacklist model for restricted Accept headers in simple CORS requests。其它的瀏覽器並沒有這個限制的實現,因為這個不在規范的內容里。
示例如下:
var invocation = new XMLHttpRequest(); var url = 'http://bar.other/resources/post-here/'; var body = '<?xml version="1.0"?><person><name>Arun</name></person>'; 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頭信息.
HTTP響應頭EDIT
這部分里列出了跨域資源共享(Cross-Origin Resource Sharing)時,服務器端需要返回的響應頭信息.上一部分內容是這部分內容在實際運用中的一個概述.
Access-Control-Allow-Origin
返回的資源需要有一個 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
Requires Gecko 2.0(Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1)
設置瀏覽器允許訪問的服務器的頭信息的白名單:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
這樣, X-My-Custom-Header
和 X-Another-Custom-Header這兩個頭信息,都可以被瀏覽器得到.
Access-Control-Max-Age
這個頭告訴我們這次預請求的結果的有效期是多久,如下:
Access-Control-Max-Age: <delta-seconds>
delta-seconds
參數表示,允許這個預請求的參數緩存的秒數,在此期間,不用發出另一條預檢請求.
Access-Control-Allow-Credentials
告知客戶端,當請求的credientials屬性是true的時候,響應是否可以被得到.當它作為預請求的響應的一部分時,它用來告知實際的請求是否使用了credentials.注意,簡單的GET請求不會預檢,所以如果一個請求是為了得到一個帶有credentials的資源,而響應里又沒有Access-Control-Allow-Credentials頭信息,那么說明這個響應被忽略了.
Access-Control-Allow-Credentials: true | false
帶有credential的請求在上面討論.
Access-Control-Allow-Methods
指明資源可以被請求的方式有哪些(一個或者多個). 這個響應頭信息在客戶端發出預檢請求的時候會被返回. 上面有相關的例子.
Access-Control-Allow-Methods: <method>[, <method>]*
發出預檢請求的例子見上,這個例子里就有向客戶端發送Access-Control-Allow-Methods響應頭信息.
Access-Control-Allow-Headers
也是在響應預檢請求的時候使用.用來指明在實際的請求中,可以使用哪些自定義HTTP請求頭.比如
Access-Control-Allow-Headers: X-PINGOTHER
這樣在實際的請求里,請求頭信息里就可以有這么一條:
X-PINGOTHER: pingpong
可以有多個自定義HTTP請求頭,用逗號分隔.
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
HTTP 請求頭EDIT
這部分內容列出來當瀏覽器發出跨域請求時可能用到的HTTP請求頭.注意這些請求頭信息都是在請求服務器的時候已經為你設置好的,當開發者使用跨域的XMLHttpRequest的時候,不需要手動的設置這些頭信息.
Origin
表明發送請求或者預請求的域
Origin: <origin>
參數origin是一個URI,告訴服務器端,請求來自哪里.它不包含任何路徑信息,只是服務器名.
注意,不僅僅是跨域請求,普通請求也會帶有ORIGIN頭信息.
Access-Control-Request-Method
在發出預檢請求時帶有這個頭信息,告訴服務器在實際請求時會使用的請求方式
Access-Control-Request-Method: <method>
相關的例子可以在這里找到
Access-Control-Request-Headers
在發出預檢請求時帶有這個頭信息,告訴服務器在實際請求時會攜帶的自定義頭信息.如有多個,可以用逗號分開.
Access-Control-Request-Headers: <field-name>[, <field-name>]*
相關的例子可以在這里找到
瀏覽器支持EDIT
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 上繪圖的支持。
相關鏈接EDIT
- Code Samples Showing
XMLHttpRequest
and Cross-Origin Resource Sharing - Cross-Origing Resource Sharing From a Server-Side Perspective (PHP, etc.)
- Cross-Origin Resource Sharing specification
XMLHttpRequest
- Further Discussion of the Origin Header
- Using CORS with All (Modern) Browsers
- Using CORS - HTML5 Rocks