MDN wants to talk to developers like you: https://qsurvey.mozilla.com/s3/8d22564490d8

所謂跨來源 HTTP 請求cross-origin HTTP request)是指發出請求所在網域不同於請求所指向之網域的 HTTP 請求,例如網域 A(http://domaina.example)的網頁載入一個 <img> 標籤的 src 屬性 向網域 B(http://domainb.foo)請求影像資源(http://domainb.foo/image.jpg)。這種作法在現今各網頁相當常見,網頁常常會載入其他網站資源,像是 CSS 樣式表、影像、程式碼等等資源。

基於安全性考量,程式碼所發出的跨來源 HTTP 請求是受到限制的。例如,XMLHttpRequestFetch 皆遵守同源政策(same-origin policy)。因此,使用 XMLHttpRequest 或 Fetch 的 Web 應用程式只能發送 HTTP 請求到其所在的網域。為了改善 Web 應用程式,開發者要求瀏覽器供應商允許跨網域請求。

跨來源資源共享(Cross-Origin Resource Sharing,簡稱 CORS)機制提供了網頁伺服器跨網域的存取控制,增加跨網域資料傳輸的安全性。現代瀏覽器支援在 API 容器(如 XMLHttpRequestFetch)中使用 CORS,以降低跨站 HTTP 請求的風險。

本文內容主要和網站管理員、伺服器端開發者和前端網頁開發者有關。現代瀏覽器會處理客戶端的跨來源共享元件,包括標頭和政策施定。關於伺服器部分請參閱伺跨來源共享:從服器觀點出發(以 PHP 為範例)的補充說明。

跨來源資源共享標準可用來開啟以下跨站 HTTP 請求:

本文主要討論跨來源資源共享與相關必要的 HTTP 標頭。

總覽

跨來源資源共享標準的運作方式是藉由加入新的 HTTP 標頭(HTTP headers)讓伺服器能夠描述來源資訊以提供予瀏覽器讀取。另外,針對會造成副作用的 HTTP 請求方法(特別是 GET 以外的 HTTP 方法,或搭配某些 MIME typesPOST 方法),規範要求瀏覽器必須要請求發送 "預檢(preflight)" 請求,以 HTTP 的 OPTIONS 方法之請求從伺服器取得其支援的方法。當伺服器許可後,再發送 HTTP 請求方法送出實際的請求。伺服器也可以通知客戶端是否要連同安全性資料(包括 Cookies 和 HTTP 驗證資料)一併隨請求送出。

之後的章節,我們將討論使用情境和相關的 HTTP 標頭。

存取控制情境範例

這裡我們會展示三種情境來說明跨來源資源共享如何運作,所有的範例都使用 XMLHttpRequest 物件,XMLHttpRequest 可以讓任何支援的瀏覽器進行跨站請求。

本節的 JavaScript 程式碼片段(以及處理跨站請求的伺服器端程式運作實體)可以在 http://arunranga.com/examples/access-control/ 看到,並可以運行在支援跨站 XMLHttpRequest 請求的瀏覽器上。

對於伺服器端的跨來源資源共享討論(包含 PHP 範例)可參考伺服器端存取控制

簡單請求

部分請求不會觸發 CORS 預檢。這類請求在本文中被稱作「簡單請求(simple requests)」,雖然 Fetch 規範(其定義了 CORS)中並不使用這個述語。一個不觸發 CORS 預檢的請求——所謂的「簡單請求(simple requests)」——其滿足以下所有條件:

備註:雖然這些都是網頁目前已經可以送出的跨站請求,但除非伺服器回傳適當標頭,否則不會有資料回傳,因此不允許跨站請求的網站無須擔心會受到新的 HTTP 存取控制影響。
備註:WebKit Nightly and Safari Technology Preview place additional restrictions on the values allowed in the Accept, Accept-Language, and Content-Language headers. If any of those headers have ”non-standard” values, WebKit/Safari does not consider the request to meet the conditions for a “simple request”. What WebKit/Safari considers “non-standard” values for those headers is not documented except in the following WebKit bugs: 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. No other browsers implement those extra restrictions, because they’re not part of the spec.

例如,假設 http://foo.example 網域上的網頁內容想要呼叫 http://bar.other 網域內的內容,以下程式碼可能會在 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.example 網域上的內容。

第 13 - 22 行是 http://bar.other 網域伺服器回傳的 HTTP 回應。第 16 行伺服器回傳了一個叫 Access-Control-Allow-Origin 的標頭,從 Origin 標頭與 Access-Control-Allow-Origin 標頭中可以看到存取控制協定最簡單的用途。這個例子中,伺服器回傳 Access-Control-Allow-Origin: * 代表允許任一網域跨站存取資源,倘若 http://bar.other 資源擁有者只准許 http://foo.example 存取資源,那麼回傳會改成:

Access-Control-Allow-Origin: http://foo.example

如此一來,origin 標頭不為 http://foo.example 網域將無法跨站存取資源。Access-Control-Allow-Origin 標頭需要包含請求 origin 標頭的值。

預檢請求

不同於前述簡單請求例子,"預檢"請求會先以HTTP的OPTIONS方法送出請求到另一個網域,確認後續真實請求是否可安全送出,由於跨站請求可能會攜帶使用者資料,所以要先進行預檢請求。

In particular, a request is preflighted if any of the following conditions is true:

Note: WebKit Nightly and Safari Technology Preview place additional restrictions on the values allowed in the Accept, Accept-Language, and Content-Language headers. If any of those headers have ”non-standard” values, WebKit/Safari preflights the request. What WebKit/Safari considers “non-standard” values for those headers is not documented except in the following WebKit bugs: 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. No other browsers implement those extra restrictions, because they’re not part of the spec.

下面是一段會引起預檢請求的範例:

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); 
    }
}

......

上方第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: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type


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, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

Once the preflight request is complete, the real request is sent:

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

<?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: 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根據前面的Javascript程式碼決定送出預檢請求,好讓伺服器回應是否允許後續送出真實請求。OPTIONS是一個HTTP/1.1方法,這個方法用來確認來自伺服器進一步的資訊,重複執行不會造成任何影響,不會造成資源更動。除了OPTIONS方法,有另外兩個請求標頭送出如下:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

Access-Control-Request-Method告訴伺服器送出真實請求的方法會是POST,Access-Control-Request-Headers告訴伺服器真實請求會攜帶一個自訂義的X-PINGOTHER標頭。在這些資訊下,接著伺服器將會確定是否接受請求。

第15~27行屬於伺服器的回應,它說明了伺服器接受POST請求方法和X-PINGOTHER標頭。另外讓我們特別來看看18~21行:

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

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天。

Preflighted requests and redirects

Most browsers currently don’t support following redirects for preflighted requests. If a redirect occurs for a preflighted request, most current browsers will report an error message such as the following.

The request was redirected to 'https://example.com/foo', which is disallowed for cross-origin requests that require preflight

Request requires preflight, which is disallowed to follow cross-origin redirect

The CORS protocol originally required that behavior but was subsquently changed to no longer require it. However, most browsers have not yet implemented the change and still exhibit the behavior that was originally required.

So until browsers catch up with the spec, you may be able to work around this limitation by doing one or both of the following:

  • change the server-side behavior to avoid the preflight and/or to avoid the redirect—if you have control over the server the request is being made to
  • change the request such that it is a simple request that doesn’t cause a preflight

But if it’s not possible to make those changes, then another way that may be possible is to this:

  1. Make a simple request to determine (using Response.url for the Fetch API, or XHR.responseURL to determine what URL the real preflighted request would end up at).
  2. Make another request (the “real” request) using the URL you obtained from Response.url or XMLHttpRequest.responseURL in the first step.

However, if the request is one that triggers a preflight due to the presence of the `Authorization` header in the request, you won’t be able to use the steps above to work around the limitation using the steps above. And you won’t be able to work around it all unless you have control over the server the request is being made to.

送出附帶安全性資料的請求

送出附帶安全性資料(HTTP Cookies和驗證資訊)的請求是XMLHttpRequest和存取控制最有趣的功能。一般來說,跨站XMLHttpRequest請求,瀏覽器不會送出這些安全性資料,不過在XMLHttpRequest物件上設定一個特定的旗標後就可以送出附帶安全性資料的請求。

下面來自http://foo.example的內容送出一個GET請求到http://bar.other索取資源,而http://bar.other會設定cookie:

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(); 
  }
}

預設不會附帶安全性資料,為了附帶,第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: 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]

雖然第12行顯示了cookie連帶被傳到http://bar.other上的資源,然而假如bar.other沒有回傳Access-Control-Allow-Credentials: true標頭(第20行),那麼bar.other的回應將被忽略且無法取得。重要事項: 當回應附帶安全性資料的請求時,伺服器必須要明白指出網域,不可以只有”*”萬用字元,像如果上面Access-Control-Allow-Origin標投如果為”*”而非http://foo.example,便不允許。第23行顯示了cookie又被設置了。

範例運作請到這裡,下面我們將看看HTTP各標頭。

HTTP 回應標頭

這邊我們將列出跨來源資源共享規範所定義伺服器端會回傳的存取控制回應標頭。

Access-Control-Allow-Origin

回應可能會附帶以下標頭格式:

Access-Control-Allow-Origin: <origin> | *

origin參數代表允許存取資源的URI。如果請求中沒有安全性資料,伺服器可以設定Access-Control-Allow-Origin為”*”,允許任何網域存取資源。

如果要允許http://mozilla.com存取伺服器,我們可以這麼做:

Access-Control-Allow-Origin: http://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告訴伺服器請求來源,不含有路徑資訊,僅有伺服器名稱。

Note: origin可為空字串,相當有用,例如來源為data URL。

這個標頭在任何存取控制請求都一定需要送出。

Access-Control-Request-Method

用在預檢請求上,告訴伺服器端後續真實請求所用的HTTP方法。

Access-Control-Request-Method: <method>

相關說明請參照本文上方。

Access-Control-Request-Headers

用在預檢請求上,告訴伺服器端後續真實請求所帶的HTTP標頭

Access-Control-Request-Headers: <field-name>[, <field-name>]*

範例說明請參考上方。

規範

Specification Status Comment
Fetch
The definition of 'CORS' in that specification.
Living Standard New definition; supplants CORS specification.
CORS Recommendation Initial definition.

瀏覽器相容性

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

相容性備註

  • IE8和IE9支援CORS透過XDomainRequest物件,IE10開始則完全正常支援。Firefox 3.5引進支援跨站XMLHttpRequests與Web Fonts,較舊版本上某些請求會受到限制。Firefox 7 引進支援跨站WebGL的跨站HTTP請求
  • While Firefox 3.5 introduced support for cross-site XMLHttpRequests and Web Fonts, certain requests were limited until later versions. Specifically, Firefox 7 introduced the ability for cross-site HTTP requests for WebGL Textures, and Firefox 9 added support for Images drawn on a canvas using drawImage.

參見

文件標籤與貢獻者

 最近更新: jackblackevo,