所謂跨站HTTP請求(Cross-site HTTP request)是指發出請求所在網域不同於請求所指向之網域的HTTP請求,例如網域A(https://domaina.example)的網頁載入一個<img>元素向網域B(https://domainb.foo)請求影像資源(https://domainb.foo/image.jpg)。這種作法在現今各網頁相當常見,網頁常常會載入其他網站資源,像是CSS樣式表、影像、程式碼等等資源。
基於安全性考量,程式碼所發出的跨站HTTP請求受到相當限制,好比說用XMLHttpRequest
物件所發出的請求受限於同源政策(same-origin policy),只能發送HTTP請求到和其所來自的相同的網域,不過開發者也希望能發展出安全的跨站請求方法來開發更好、更安全、搭配多種資源的網頁應用。
W3C下轄的網頁應用工作小組(Web Applications Working Group)建議採用跨來源資源共享(Cross-Origin Resource Sharing, CORS)來達成安全的跨站資料傳輸。CORS提供網頁伺服器支援跨站存取控制方法,另外,這份規範是用於API容器,例如以XMLHttpRequest作為調解機制,好跳脫同網域限制;也因為客戶端瀏覽器會多出新的跨來源共享元件,包括標頭和政策施定,所以說,伺服器也必須相應地處理這些新增機制,也就是處理新標頭以及用新標頭發送資源。本文內容主要和網站管理員、伺服器端開發者和網站開發者有關,然後關於伺服器部分請參閱伺跨來源共享:從服器觀點出發(以PHP為範例)補充文章。
跨來源資源共享標準可用來開啟以下跨站HTTP請求:
- 用XMLHttpRequest API進行跨站請求,如前所述。
- 網頁字體(跨網域CSS的@font-face的字體用途),所以伺服器可以部屬TrueType字體並只允許授權網站進行跨站載入使用。
- WebGL紋理
- 用drawImage畫到畫布上的影像
本文主要在總覽跨來源資源共享和FireFox 3.5所實作的HTTP標頭。
總覽
跨來源資源共享標準加入了新HTTP標頭,使得伺服器能夠描述那些來源可以用瀏覽器讀取資料。另外,針對會造成副作用的HTTP請求方法(特別是GET以外的HTTP方法或搭配某些MIME種類的POST方法),標準規範了瀏覽器必須要發送「先導」請求,以HTTP的OPTIONS方法從伺服器取得支援方法,然後當伺服器許可後再用真實的HTTP請求方法送出真實的請求。伺服器也可以通知客戶端是否要連安全性資料(包括cookie和HTTP驗證資料)一併隨請求送出。
後面我們將討論相關情境和相關HTTP標頭。
存取控制情境範例
這邊我們會展示三種情境說明跨來源資源共享如何運作,所有的跨站請求範例都使用XMLHttpRequest
物件。
本處Javascript程式碼範本要運行在支援跨站XMHttpRequest 請求的瀏覽器上,如果想看Javascript程式(以及處理跨站請求的伺服器端程式運作實體)的實際運行請到這裡,如果想了解伺服器端的跨來源資源共享探討(包含PHP程式碼範例)請到這裡。
簡單的請求
所謂簡單的跨站請求意味著:
- 只用GET, HEAD, POST方法
- 標頭必須為下列類型:Accept, Accept-Language, Content-Language(不區分大小寫),或者是Content-Type必須為下列application/x-www-form-urlencoded, multipart/form-data, 或text/plain其中一種。
- 沒有自訂義的標頭,例如X-Modified等等。
假設https://foo.example網域上的網頁內容想要呼叫https://bar.other網域內的內容,以下程式碼可能會在foo.example上執行:
var invocation = new XMLHttpRequest(); var url = 'https://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: https://foo.example/examples/access-control/simpleXSInvocation.html Origin: https://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標頭,它標示出請求是來自https://foo.example網域上的內容。
第13 ~ 22行是https://bar.other網域伺服器回傳的HTTP回應。第16行伺服器回傳了一個叫Access-Control-Allow-Origin的標頭,從origin標頭與Access-Control-Allow-Origin標頭中可以看到存取控制協定最簡單的用途。這個例子中,伺服器回傳” Access-Control-Allow-Origin: *”代表允許任一網域跨站存取資源,倘若https://bar.other資源擁有者只准許https://foo.example存取資源,那麼回傳會改成:
Access-Control-Allow-Origin: https://foo.example
如此一來,origin標頭不為https://foo.example網域將無法跨站存取資源。Access-Control-Allow-Origin標頭需要包含請求origin標頭的值。
先導請求
不同於前述簡單請求例子,"先導"請求會先以HTTP的OPTIONS方法送出請求到另一個網域,確認後續真實請求是否可安全送出,由於跨站請求可能會攜帶使用者資料,所以要先進行先導請求。先導請求會被送出當符合以下其中一種條件時:
- 使用非簡單方法(GET, HEAD, POST)。
- 使用以下類型除外的標頭:Accept, Accept-Language, Content-Language,Content-Type非為下列application/x-www-form-urlencoded, multipart/form-data, 或text/plain其中一種(上述類型為簡單標頭,且皆不區分大小寫)。例如POST請求連帶的資料是application/xml或text/xml的XML類型資料,那麼先導請求就會先送出。
- 請求中有自訂義標頭,例如自訂義一個X-PINGOTHER標頭。
自Gecko 2.0(Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1)起,text/plain, application/x-www-form-urlencoded與multipart/form-data編碼資料都不需要先導請求,在之前只有text/plain不需要先導。
下面是一段會引起先導請求的範例:
var invocation = new XMLHttpRequest(); var url = 'https://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); } ......
上方第3行產生一段XML類型資料,然後第8行POST請求送出,第9行自訂義一個不屬於HTTP/1.1協定的X-PINGOTHER: pingpong標頭,因為POST送出Content-type為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: https://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: https://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER Access-Control-Max-Age: 1728000 Vary: Accept-Encoding 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: https://foo.example/examples/preflightInvocation.html Content-Length: 55 Origin: https://foo.example Pragma: no-cache Cache-Control: no-cache <?xml version="1.0"?><person><name>Arun</name></person> HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:40 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: https://foo.example Vary: Accept-Encoding 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根據前面的Javascript程式碼決定送出先導請求,好讓伺服器回應是否允許後續送出真實請求。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行屬於伺服器的回應,它說明了伺服器接受POST請求方法和X-PINGOTHER標頭。另外讓我們特別來看看18~21行:
Access-Control-Allow-Origin: https://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: 回應標頭十分相似,但它只在存取控制範圍下才有意義。Access-Control-Allow-Headers顯示了X-PINGOTHER為可接受標頭,和Access-Control-Allow-Methods一樣,Access-Control-Allow-Headers也是用逗號分隔可接受標頭。Access-Control-Max-Age顯示了本次先導請求回應所可以快取的秒數,其中1728000秒等於20天。
送出附帶安全性資料的請求
送出附帶安全性資料(HTTP Cookies和驗證資訊)的請求是XMLHttpRequest
和存取控制最有趣的功能。一般來說,跨站XMLHttpRequest請求,瀏覽器不會送出這些安全性資料,不過在XMLHttpRequest物件上設定一個特定的旗標後就可以送出附帶安全性資料的請求。
下面來自https://foo.example的內容送出一個GET請求到https://bar.other索取資源,而https://bar.other會設定cookie:
var invocation = new XMLHttpRequest(); var url = 'https://bar.other/resources/credentialed-content/'; function callOtherDomain(){ if(invocation) { invocation.open('GET', url, true); invocation.withCredentials = true; invocation.onreadystatechange = handler; invocation.send(); }
預設不會附帶安全性資料,為了附帶,第7行上XMLHttpRequest有一個withCredentials旗標必須要設定為true,而且由於這只是一個簡單的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: https://foo.example/examples/credential.html Origin: https://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: https://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 Content-Encoding: gzip Content-Length: 106 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain [text/plain payload]
雖然第12行顯示了cookie連帶被傳到https://bar.other上的資源,然而假如bar.other沒有回傳Access-Control-Allow-Credentials: true標頭(第20行),那麼bar.other的回應將被忽略且無法取得。重要事項: 當回應附帶安全性資料的請求時,伺服器必須要明白指出網域,不可以只有”*”萬用字元,像如果上面Access-Control-Allow-Origin標投如果為”*”而非https://foo.example,便不允許。第23行顯示了cookie又被設置了。
範例運作請到這裡,下面我們將看看HTTP各標頭。
HTTP回應標頭
這邊我們將列出跨來源資源共享規範所定義伺服器端會回傳的存取控制回應標頭。
Access-Control-Allow-Origin
回應可能會附帶以下標頭格式:
Access-Control-Allow-Origin: <origin> | *
origin參數代表允許存取資源的URI。如果請求中沒有安全性資料,伺服器可以設定Access-Control-Allow-Origin為”*”,允許任何網域存取資源。
如果要允許https://mozilla.com存取伺服器,我們可以這麼做:
Access-Control-Allow-Origin: https://mozilla.com
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
這個標頭指示了當請求的credentials設定true時,是否要回應請求。當用在先導請求的回應中,那就是指示後續的真實請求可否附帶安全性資料。由於簡單的GET請求沒有先導請求,所以如果附帶安全性資料的GET請求沒有受到這個標頭的回應,那回應將被忽略而且無法取得。
Access-Control-Allow-Credentials: true | false
Access-Control-Allow-Methods
指示存取資源所允許的方法,用來回應先導請求。
Access-Control-Allow-Methods: <method>[, <method>]*
Access-Control-Allow-Headers
指示那些HTTP header可以出現在真實請求,用於回應先導請求。
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
HTTP 請求標頭
這裡列出當進行跨來源資源共享請求客戶端需要送出的標頭。請注意使用跨站XMLHttpRequest
不需要在程式碼中設定這些標頭,這些標頭會由瀏覽器來設定。
Origin
指示跨站存取請求或先導請求的來源。
Origin: <origin>
其值是一個URI告訴伺服器請求來源,不含有路徑資訊,僅有伺服器名稱。
這個標頭在任何存取控制請求都一定需要送出。
Access-Control-Request-Method
用在先導請求上,告訴伺服器端後續真實請求所用的HTTP方法。
Access-Control-Request-Method: <method>
Access-Control-Request-Headers
用在先導請求上,告訴伺服器端後續真實請求所帶的HTTP標頭
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 |
Feature | Android | Chrome for Android | Firefox Mobile (Gecko) | IE Mobile | Opera Mobile | Safari Mobile |
---|---|---|---|---|---|---|
Basic support | 2.1 | yes | yes | ? | 12 | 3.2 |
Note
IE8和IE9支援CORS透過XDomainRequest物件,IE10開始則完全正常支援。Firefox 3.5引進支援跨站XMLHttpRequests與Web Fonts,較舊版本上某些請求會受到限制。Firefox 7 引進支援跨站WebGL的跨站HTTP請求
延伸閱讀
- 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