MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

翻譯不完整。請協助 翻譯此英文文件

所謂跨來源 HTTP 請求cross-origin HTTP request)是指發出請求所在網域不同於請求所指向之網域的 HTTP 請求,例如網域 A(http://domain-a.com)的網頁載入一個 <img> 標籤的 src 屬性 向網域 B(http://domain-b.com)請求影像資源(http://domain-b.com/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 行之主要 HTTP 請求標頭中的 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

如此一來,來源並非 http://foo.example 網域(由第 10 行請求標頭中的 ORIGIN 標頭確認)便無法以跨站的方式存取資源。Access-Control-Allow-Origin 標頭必須包含請求當中的 Origin 標頭值。

預檢請求

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

準確來說,如果滿足以下任一項條件時會發出預檢請求:

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 請求標頭(X-PINGOTHER: pingpong)。此標頭並非 HTTP/1.1 通訊協定的一部分,但廣泛的使用於 Web 應用程式。而因為請求的 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

一旦預檢請求完成,真正的請求才會被送出:

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 方法的預檢請求,瀏覽器依據前面的 JavaScript 程式碼決定送出預檢請求,好讓伺服器回應是否允許後續送出真實請求。OPTIONS 是一個 HTTP/1.1 方法,這個方法用來確認來自伺服器進一步的資訊,重複執行不會造成任何影響,為一安全方法,不會造成資源更動。除了 OPTIONS 方法,有另外兩個送出的請求標頭(分別在第 10 及 11 行):

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

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

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

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 標頭表示伺服器可以接受 POSTGETOPTIONS 方法。請注意此標頭和 Allow 十分相似,但它只在存取控制範圍下才有意義。

伺服器也回傳了 Access-Control-Allow-Headers 標頭及其值「X-PINGOTHER, Content-Type」,表示伺服器允許在真實請求中使用以上這兩個標頭。與 Access-Control-Allow-Methods 相同,Access-Control-Allow-Headers 也是用逗號分隔可接受的標頭名稱。

最後,Access-Control-Max-Age 提供了本次預檢請求回應所可以快取的秒數。在此範例中,86400 秒即為 24 小時。請留意每一個瀏覽器都有預設的最大值,當 Access-Control-Max-Age 較預設值大時會優先採用預設值。

預檢請求和重新導向

目前大多瀏覽器不支援預檢請求時的重新導向,如果預檢請求進行中發生重新導向,目前大多的瀏覽器會回報類似以下的錯誤訊息。

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

CORS 通訊協定最初要求此預檢請求重新導向的行為,但在隨後的修訂中即改為不要求使用。然而,大多數的瀏覽器尚未實作此變動,且仍舊依照原本的行為要求。

因此直到瀏覽器趕上規範之前,你可以使用下列一或兩種方法來解決這個限制:

  • 變更伺服器端的行為以避免預檢以及/或是避免重新導向——假如你對被請求的伺服擁有控制權
  • 變更請求為簡單請求,讓預檢不會發生

但若難以實施以上方法,仍有其他可行的方式:

  1. 建立一個簡單請求來測定(使用 Fetch API 的 Response.urlXHR.responseURL 來測定真實預檢請求最終導向的 URL)。
  2. 建立另一個請求(「真實的」請求)發送至第一步自 Response.urlXHR.responseURL 所獲得的 URL。

然而,假如請求是由於存在 Authorization 標頭而觸發預檢,便無法利用以上的步驟來解除限制。並且直到你對被請求的伺服擁有控制權前,沒有其他方式能夠解決。

附帶身分憑證的請求

送出附帶安全性資料(HTTP Cookies 和驗證資訊)的請求為 XMLHttpRequestFetch 在存取控制最有趣的功能。一般來說,跨站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又被設置了。

Credentialed requests and wildcards

When responding to a credentialed request, the server must specify an origin in the value of the Access-Control-Allow-Origin header, instead of specifying the "*" wildcard.

Because the request headers in the above example include a Cookie header, the request would fail if the value of the Access-Control-Allow-Origin header were "*". But it does not fail: Because the value of the Access-Control-Allow-Origin header is "http://foo.example" (an actual origin) rather than the "*" wildcard, the credential-cognizant content is returned to the invoking web content.

Note that the Set-Cookie response header in the example above also sets a further cookie. In case of failure, an exception—depending on the API used—is raised.

範例運作請到這裡,下面我們將看看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.

瀏覽器相容性

FeatureChromeEdgeFirefoxInternet ExplorerOperaSafari
Basic Support4123.510124
FeatureAndroidChrome for AndroidEdge mobileFirefox for AndroidIE mobileOpera AndroidiOS Safari
Basic Support2.1(Yes)(Yes)1.0(Yes)123.2

相容性備註

  • IE8 和 IE9 支援 CORS 透過 XDomainRequest 物件,IE10 開始則完全正常支援。
  • Firefox 3.5 引進支援跨站 XMLHttpRequests 與 Web Fonts,較舊版本上某些請求會受到限制。Firefox 7 引進支援 WebGL 紋理的跨站 HTTP 請求,而 Firefox 9 新增支援使用 drawImage 方法將圖形繪製於 canvas 中。

參見

文件標籤與貢獻者

 最近更新: jackblackevo,