如果我們向另一個(gè)網(wǎng)站發(fā)送 ?fetch
? 請(qǐng)求,則該請(qǐng)求可能會(huì)失敗。
例如,讓我們嘗試向 http://example.com
發(fā)送 fetch
請(qǐng)求:
try {
await fetch('http://example.com');
} catch(err) {
alert(err); // fetch 失敗
}
正如所料,獲取失敗。
這里的核心概念是 源(origin)—— 域(domain)/端口(port)/協(xié)議(protocol)的組合。
跨源請(qǐng)求 —— 那些發(fā)送到其他域(即使是子域)、協(xié)議或端口的請(qǐng)求 —— 需要來(lái)自遠(yuǎn)程端的特殊 header。
這個(gè)策略被稱(chēng)為 “CORS”:跨源資源共享(Cross-Origin Resource Sharing)。
CORS 的存在是為了保護(hù)互聯(lián)網(wǎng)免受黑客攻擊。
說(shuō)真的,在這說(shuō)點(diǎn)兒題外話,講講它的歷史。
多年來(lái),來(lái)自一個(gè)網(wǎng)站的腳本無(wú)法訪問(wèn)另一個(gè)網(wǎng)站的內(nèi)容。
這個(gè)簡(jiǎn)單有力的規(guī)則是互聯(lián)網(wǎng)安全的基礎(chǔ)。例如,來(lái)自 hacker.com
的腳本無(wú)法訪問(wèn) gmail.com
上的用戶(hù)郵箱。基于這樣的規(guī)則,人們感到很安全。
在那時(shí)候,JavaScript 并沒(méi)有任何特殊的執(zhí)行網(wǎng)絡(luò)請(qǐng)求的方法。它只是一種用來(lái)裝飾網(wǎng)頁(yè)的玩具語(yǔ)言而已。
但是 Web 開(kāi)發(fā)人員需要更多功能。人們發(fā)明了各種各樣的技巧去突破該限制,并向其他網(wǎng)站發(fā)出請(qǐng)求。
其中一種和其他服務(wù)器通信的方法是在那里提交一個(gè) <form>
。人們將它提交到 <iframe>
,只是為了停留在當(dāng)前頁(yè)面,像這樣:
<!-- 表單目標(biāo) -->
<iframe name="iframe"></iframe>
<!-- 表單可以由 JavaScript 動(dòng)態(tài)生成并提交 -->
<form target="iframe" method="POST" action="http://another.com/…">
...
</form>
因此,即使沒(méi)有網(wǎng)絡(luò)方法,也可以向其他網(wǎng)站發(fā)出 GET/POST 請(qǐng)求,因?yàn)楸韱慰梢詫?shù)據(jù)發(fā)送到任何地方。但是由于禁止從其他網(wǎng)站訪問(wèn) <iframe>
中的內(nèi)容,因此就無(wú)法讀取響應(yīng)。
確切地說(shuō),實(shí)際上有一些技巧能夠解決這個(gè)問(wèn)題,這在 iframe 和頁(yè)面中都需要添加特殊腳本。因此,與 iframe 的通信在技術(shù)上是可能的?,F(xiàn)在我們沒(méi)必要講其細(xì)節(jié)內(nèi)容,我們還是讓這些古董代碼不要再出現(xiàn)了吧。
另一個(gè)技巧是使用 script
標(biāo)簽。script
可以具有任何域的 src
,例如 <script src="http://another.com/…" rel="external nofollow" >
。也可以執(zhí)行來(lái)自任何網(wǎng)站的 script
。
如果一個(gè)網(wǎng)站,例如 another.com
試圖公開(kāi)這種訪問(wèn)方式的數(shù)據(jù),則會(huì)使用所謂的 “JSONP (JSON with padding)” 協(xié)議。
這是它的工作方式。
假設(shè)在我們的網(wǎng)站,需要以這種方式從 http://another.com
網(wǎng)站獲取數(shù)據(jù),例如天氣:
gotWeather
?。// 1. 聲明處理天氣數(shù)據(jù)的函數(shù)
function gotWeather({ temperature, humidity }) {
alert(`temperature: ${temperature}, humidity: ${humidity}`);
}
src="http://another.com/weather.json?callback=gotWeather" rel="external nofollow"
的 <script>
標(biāo)簽,使用我們的函數(shù)名作為它的 callback
URL-參數(shù)。let script = document.createElement('script');
script.src = `http://another.com/weather.json?callback=gotWeather`;
document.body.append(script);
another.com
動(dòng)態(tài)生成一個(gè)腳本,該腳本調(diào)用 gotWeather(...)
,發(fā)送它想讓我們接收的數(shù)據(jù)。// 我們期望來(lái)自服務(wù)器的回答看起來(lái)像這樣:
gotWeather({
temperature: 25,
humidity: 78
});
gotWeather
? 函數(shù)將運(yùn)行,并且因?yàn)樗俏覀兊暮瘮?shù),我們就有了需要的數(shù)據(jù)。這是可行的,并且不違反安全規(guī)定,因?yàn)殡p方都同意以這種方式傳遞數(shù)據(jù)。而且,既然雙方都同意這種行為,那這肯定不是黑客攻擊了。現(xiàn)在仍然有提供這種訪問(wèn)的服務(wù),因?yàn)榧词故欠浅Ef的瀏覽器它依然適用。
不久之后,網(wǎng)絡(luò)方法出現(xiàn)在了瀏覽器 JavaScript 中。
起初,跨源請(qǐng)求是被禁止的。但是,經(jīng)過(guò)長(zhǎng)時(shí)間的討論,跨源請(qǐng)求被允許了,但是任何新功能都需要服務(wù)器明確允許,以特殊的 header 表述。
有兩種類(lèi)型的跨源請(qǐng)求:
安全請(qǐng)求很簡(jiǎn)單,所以我們先從它開(kāi)始。
如果一個(gè)請(qǐng)求滿(mǎn)足下面這兩個(gè)條件,則該請(qǐng)求是安全的:
Accept
?,Accept-Language
?,Content-Language
?,Content-Type
? 的值為 ?application/x-www-form-urlencoded
?,?multipart/form-data
? 或 ?text/plain
?。任何其他請(qǐng)求都被認(rèn)為是“非安全”請(qǐng)求。例如,具有 PUT
方法或 API-Key
HTTP-header 的請(qǐng)求就不是安全請(qǐng)求。
本質(zhì)區(qū)別在于,可以使用 <form>
或 <script>
進(jìn)行安全請(qǐng)求,而無(wú)需任何其他特殊方法。
因此,即使是非常舊的服務(wù)器也能很好地接收安全請(qǐng)求。
與此相反,帶有非標(biāo)準(zhǔn) header 或者例如 DELETE
方法的請(qǐng)求,無(wú)法通過(guò)這種方式創(chuàng)建。在很長(zhǎng)一段時(shí)間里,JavaScript 都不能進(jìn)行這樣的請(qǐng)求。所以,舊的服務(wù)器可能會(huì)認(rèn)為此類(lèi)請(qǐng)求來(lái)自具有特權(quán)的來(lái)源(privileged source),“因?yàn)榫W(wǎng)頁(yè)無(wú)法發(fā)送它們”。
當(dāng)我們嘗試發(fā)送一個(gè)非安全請(qǐng)求時(shí),瀏覽器會(huì)發(fā)送一個(gè)特殊的“預(yù)檢(preflight)”請(qǐng)求到服務(wù)器 —— 詢(xún)問(wèn)服務(wù)器,你接受此類(lèi)跨源請(qǐng)求嗎?
并且,除非服務(wù)器明確通過(guò) header 進(jìn)行確認(rèn),否則非安全請(qǐng)求不會(huì)被發(fā)送。
現(xiàn)在,我們來(lái)詳細(xì)介紹它們。
如果一個(gè)請(qǐng)求是跨源的,瀏覽器始終會(huì)向其添加 Origin
header。
例如,如果我們從 https://javascript.info/page
請(qǐng)求 https://anywhere.com/request
,請(qǐng)求的 header 將如下所示:
GET /request
Host: anywhere.com
Origin: https://javascript.info
...
正如你所看到的,Origin
包含了確切的源(domain/protocol/port),沒(méi)有路徑(path)。
服務(wù)器可以檢查 Origin
,如果同意接受這樣的請(qǐng)求,就會(huì)在響應(yīng)中添加一個(gè)特殊的 header Access-Control-Allow-Origin
。該 header 包含了允許的源(在我們的示例中是 https://javascript.info
),或者一個(gè)星號(hào) *
。然后響應(yīng)成功,否則報(bào)錯(cuò)。
瀏覽器在這里扮演受被信任的中間人的角色:
Origin
?。Access-Control-Allow-Origin
?,如果存在,則允許 JavaScript 訪問(wèn)響應(yīng),否則將失敗并報(bào)錯(cuò)。
這是一個(gè)帶有服務(wù)器許可的響應(yīng)示例:
200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascript.info
對(duì)于跨源請(qǐng)求,默認(rèn)情況下,JavaScript 只能訪問(wèn)“安全的” response header:
Cache-Control
?Content-Language
?Content-Type
?Expires
?Last-Modified
?Pragma
?訪問(wèn)任何其他 response header 都將導(dǎo)致 error。
請(qǐng)注意:
請(qǐng)注意:列表中沒(méi)有
Content-Length
header!
該 header 包含完整的響應(yīng)長(zhǎng)度。因此,如果我們正在下載某些內(nèi)容,并希望跟蹤進(jìn)度百分比,則需要額外的權(quán)限才能訪問(wèn)該 header(請(qǐng)見(jiàn)下文)。
要授予 JavaScript 對(duì)任何其他 response header 的訪問(wèn)權(quán)限,服務(wù)器必須發(fā)送 Access-Control-Expose-Headers
header。它包含一個(gè)以逗號(hào)分隔的應(yīng)該被設(shè)置為可訪問(wèn)的非安全 header 名稱(chēng)列表。
例如:
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Length,API-Key
有了這種 Access-Control-Expose-Headers
header,此腳本就被允許讀取響應(yīng)的 Content-Length
和 API-Key
header。
我們可以使用任何 HTTP 方法:不僅僅是 GET/POST
,也可以是 PATCH
,DELETE
及其他。
之前,沒(méi)有人能夠設(shè)想網(wǎng)頁(yè)能發(fā)出這樣的請(qǐng)求。因此,可能仍然存在有些 Web 服務(wù)將非標(biāo)準(zhǔn)方法視為一個(gè)信號(hào):“這不是瀏覽器”。它們可以在檢查訪問(wèn)權(quán)限時(shí)將其考慮在內(nèi)。
因此,為了避免誤解,任何“非安全”請(qǐng)求 —— 在過(guò)去無(wú)法完成的,瀏覽器不會(huì)立即發(fā)出此類(lèi)請(qǐng)求。首先,它會(huì)先發(fā)送一個(gè)初步的、所謂的“預(yù)檢(preflight)”請(qǐng)求,來(lái)請(qǐng)求許可。
預(yù)檢請(qǐng)求使用 OPTIONS
方法,它沒(méi)有 body,但是有三個(gè) header:
Access-Control-Request-Method header
? 帶有非安全請(qǐng)求的方法。Access-Control-Request-Headers header
? 提供一個(gè)以逗號(hào)分隔的非安全 HTTP-header 列表。如果服務(wù)器同意處理請(qǐng)求,那么它會(huì)進(jìn)行響應(yīng),此響應(yīng)的狀態(tài)碼應(yīng)該為 200,沒(méi)有 body,具有 header:
Access-Control-Allow-Origin
? 必須為 ?*
? 或進(jìn)行請(qǐng)求的源(例如 ?https://javascript.info
?)才能允許此請(qǐng)求。Access-Control-Allow-Methods
? 必須具有允許的方法。Access-Control-Allow-Headers
? 必須具有一個(gè)允許的 header 列表。Access-Control-Max-Age
? 可以指定緩存此權(quán)限的秒數(shù)。因此,瀏覽器不是必須為滿(mǎn)足給定權(quán)限的后續(xù)請(qǐng)求發(fā)送預(yù)檢。
讓我們?cè)谝粋€(gè)跨源 PATCH
請(qǐng)求的例子中一步一步地看它是如何工作的(此方法經(jīng)常被用于更新數(shù)據(jù)):
let response = await fetch('https://site.com/service.json', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'API-Key': 'secret'
}
});
這里有三個(gè)理由解釋為什么它不是一個(gè)安全請(qǐng)求(其實(shí)一個(gè)就夠了):
PATCH
?Content-Type
? 不是這三個(gè)中之一:?application/x-www-form-urlencoded
?,?multipart/form-data
?,?text/plain
?。API-Key
? header。在發(fā)送我們的請(qǐng)求前,瀏覽器會(huì)自己發(fā)送如下所示的預(yù)檢請(qǐng)求:
OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type,API-Key
OPTIONS
?。/service.json
?。Origin
? —— 來(lái)源。Access-Control-Request-Method
? —— 請(qǐng)求方法。Access-Control-Request-Headers
? —— 以逗號(hào)分隔的“非安全” header 列表。服務(wù)應(yīng)響應(yīng)狀態(tài) 200 和 header:
Access-Control-Allow-Origin: https://javascript.info
?Access-Control-Allow-Methods: PATCH
?Access-Control-Allow-Headers: Content-Type,API-Key
?。這將允許后續(xù)通信,否則會(huì)觸發(fā)錯(cuò)誤。
如果服務(wù)器將來(lái)需要其他方法和 header,則可以通過(guò)將這些方法和 header 添加到列表中來(lái)預(yù)先允許它們。
例如,此響應(yīng)還允許 ?PUT
?、?DELETE
? 以及其他 header:
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400
現(xiàn)在,瀏覽器可以看到 PATCH
在 Access-Control-Allow-Methods
中,Content-Type,API-Key
在列表 Access-Control-Allow-Headers
中,因此它將發(fā)送主請(qǐng)求。
如果 Access-Control-Max-Age
帶有一個(gè)表示秒的數(shù)字,則在給定的時(shí)間內(nèi),預(yù)檢權(quán)限會(huì)被緩存。上面的響應(yīng)將被緩存 86400 秒,也就是一天。在此時(shí)間范圍內(nèi),后續(xù)請(qǐng)求將不會(huì)觸發(fā)預(yù)檢。假設(shè)它們符合緩存的配額,則將直接發(fā)送它們。
預(yù)檢成功后,瀏覽器現(xiàn)在發(fā)出主請(qǐng)求。這里的過(guò)程與安全請(qǐng)求的過(guò)程相同。
主請(qǐng)求具有 Origin
header(因?yàn)樗强缭吹模?
PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascript.info
服務(wù)器不應(yīng)該忘記在主響應(yīng)中添加 Access-Control-Allow-Origin
。成功的預(yù)檢并不能免除此要求:
Access-Control-Allow-Origin: https://javascript.info
然后,JavaScript 可以讀取主服務(wù)器響應(yīng)了。
請(qǐng)注意:
預(yù)檢請(qǐng)求發(fā)生在“幕后”,它對(duì) JavaScript 不可見(jiàn)。
JavaScript 僅獲取對(duì)主請(qǐng)求的響應(yīng),如果沒(méi)有服務(wù)器許可,則獲得一個(gè) error。
默認(rèn)情況下,由 JavaScript 代碼發(fā)起的跨源請(qǐng)求不會(huì)帶來(lái)任何憑據(jù)(cookies 或者 HTTP 認(rèn)證(HTTP authentication))。
這對(duì)于 HTTP 請(qǐng)求來(lái)說(shuō)并不常見(jiàn)。通常,對(duì) http://site.com
的請(qǐng)求附帶有該域的所有 cookie。但是由 JavaScript 方法發(fā)出的跨源請(qǐng)求是個(gè)例外。
例如,fetch('http://another.com')
不會(huì)發(fā)送任何 cookie,即使那些 (!) 屬于 another.com
域的 cookie。
為什么?
這是因?yàn)榫哂袘{據(jù)的請(qǐng)求比沒(méi)有憑據(jù)的請(qǐng)求要強(qiáng)大得多。如果被允許,它會(huì)使用它們的憑據(jù)授予 JavaScript 代表用戶(hù)行為和訪問(wèn)敏感信息的全部權(quán)力。
服務(wù)器真的這么信任這種腳本嗎?是的,它必須顯式地帶有允許請(qǐng)求的憑據(jù)和附加 header。
要在 fetch
中發(fā)送憑據(jù),我們需要添加 credentials: "include"
選項(xiàng),像這樣:
fetch('http://another.com', {
credentials: "include"
});
現(xiàn)在,fetch
將把源自 another.com
的 cookie 和我們的請(qǐng)求發(fā)送到該網(wǎng)站。
如果服務(wù)器同意接受 帶有憑據(jù) 的請(qǐng)求,則除了 Access-Control-Allow-Origin
外,服務(wù)器還應(yīng)該在響應(yīng)中添加 header Access-Control-Allow-Credentials: true
。
例如:
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
請(qǐng)注意:對(duì)于具有憑據(jù)的請(qǐng)求,禁止 Access-Control-Allow-Origin
使用星號(hào) *
。如上所示,它必須有一個(gè)確切的源。這是另一項(xiàng)安全措施,以確保服務(wù)器真的知道它信任的發(fā)出此請(qǐng)求的是誰(shuí)。
從瀏覽器角度來(lái)看,有兩種跨源請(qǐng)求:“安全”請(qǐng)求和其他請(qǐng)求。
“安全”請(qǐng)求必須滿(mǎn)足以下條件:
Accept
?Accept-Language
?Content-Language
?Content-Type
? 的值為 ?application/x-www-form-urlencoded
?,?multipart/form-data
? 或 ?text/plain
?。安全請(qǐng)求和其他請(qǐng)求的本質(zhì)區(qū)別在于,自古以來(lái)就可以使用 <form>
或 <script>
標(biāo)簽來(lái)實(shí)現(xiàn)安全請(qǐng)求,而對(duì)于瀏覽器來(lái)說(shuō),非安全請(qǐng)求在很長(zhǎng)一段時(shí)間都是不可能的。
所以,實(shí)際區(qū)別在于,安全請(qǐng)求會(huì)立即發(fā)送,并帶有 Origin
header,而對(duì)于其他請(qǐng)求,瀏覽器會(huì)發(fā)出初步的“預(yù)檢”請(qǐng)求,以請(qǐng)求許可。
對(duì)于安全請(qǐng)求:
Origin
? header。Access-Control-Allow-Origin
? 為 ?*
? 或與 ?Origin
? 的值相同Access-Control-Allow-Origin
? 值與 ?Origin
? 的相同Access-Control-Allow-Credentials
? 為 ?true
?此外,要授予 JavaScript 訪問(wèn)除 Cache-Control
,Content-Language
,Content-Type
,Expires
,Last-Modified
或 Pragma
外的任何 response header 的權(quán)限,服務(wù)器應(yīng)該在 header Access-Control-Expose-Headers
中列出允許的那些
header。
對(duì)于非安全請(qǐng)求,會(huì)在請(qǐng)求之前發(fā)出初步“預(yù)檢”請(qǐng)求:
OPTIONS
? 請(qǐng)求發(fā)送到相同的 URL:Access-Control-Request-Method
? 有請(qǐng)求方法。Access-Control-Request-Headers
? 以逗號(hào)分隔的“非安全” header 列表。Access-Control-Allow-Methods
? 帶有允許的方法的列表,Access-Control-Allow-Headers
? 帶有允許的 header 的列表,Access-Control-Max-Age
? 帶有指定緩存權(quán)限的秒數(shù)。你可能知道有一個(gè) HTTP-header Referer
,它通常包含發(fā)起網(wǎng)絡(luò)請(qǐng)求的頁(yè)面的 url。
例如,當(dāng)從 http://javascript.info/some/url
fetch http://google.com
時(shí),header 看起來(lái)如下:
Accept: */*
Accept-Charset: utf-8
Accept-Encoding: gzip,deflate,sdch
Connection: keep-alive
Host: google.com
Origin: http://javascript.info
Referer: http://javascript.info/some/url
正如你所看到的,存在 Referer
和 Origin
。
問(wèn)題是:
Origin
?,如果 ?Referer
? 甚至具有更多信息?Referer
? 或 ?Origin
? 可行嗎,還是說(shuō)會(huì)出問(wèn)題?我們需要 Origin
,是因?yàn)橛袝r(shí)會(huì)沒(méi)有 Referer
。例如,當(dāng)我們從 HTTPS(從高安全性訪問(wèn)低安全性)fetch
HTTP 頁(yè)面時(shí),便沒(méi)有 Referer
。
內(nèi)容安全策略 可能會(huì)禁止發(fā)送 Referer
。
正如我們將看到的,fetch
也具有阻止發(fā)送 Referer
的選項(xiàng),甚至允許修改它(在同一網(wǎng)站內(nèi))。
根據(jù)規(guī)范,Referer
是一個(gè)可選的 HTTP-header。
正是因?yàn)?nbsp;Referer
不可靠,才發(fā)明了 Origin
。瀏覽器保證跨源請(qǐng)求的正確 Origin
。
更多建議: