跨域是前端開發(fā)中一個非常常見的問題永品,尤其是隨著單頁應(yīng)用(Single Page Application, SPA)的興起,前后端分離開發(fā)和部署立莉,前端在本地開發(fā)和部署的過程中都會面臨著跨域問題厕诡。我們再次聊聊跨域這個話題描馅,以及項目中對跨域的一些實踐經(jīng)驗卿吐,希望帶來一些新的收獲旁舰。
什么是跨域
首先我們需要了解下什么是同源策略,MDN 中是這樣介紹的:
同源策略是瀏覽器的一個重要安全策略嗡官,它用于限制一個源的文檔或者它加載的腳本如何能與另一個源的資源進(jìn)行操作箭窜。它能幫助阻隔惡意文檔,減少可能被攻擊的媒介衍腥。
從這段話我們得知磺樱,同源策略是瀏覽器的安全策略,源(Origin)是判斷是否滿足同源策略的條件婆咸。我們常說的跨域竹捉,準(zhǔn)確來說應(yīng)該是跨源,目的是不受同源策略的限制去操作另一個源的資源尚骄。例如當(dāng)我們在本地開發(fā)時块差,源是 http://localhost:3000
,而服務(wù)端接口的地址是 https://api.feishu.cn
倔丈,此時通過 Fetch API
訪問接口就會產(chǎn)生跨域憨闰。
同源策略源自于瀏覽器,反之在非瀏覽器的環(huán)境下需五,一般是沒有同源策略限制的鹉动。例如在 Node.js 中,我們可以請求任意的網(wǎng)址并得到結(jié)果宏邮。因此基于 Node.js 的服務(wù)端可以直接跨域訪問資源训裆。
當(dāng)然我們也可以關(guān)閉瀏覽器的同源策略,關(guān)閉后瀏覽器環(huán)境下也可以直接跨域蜀铲。以 Chrome 為例,通過給 Chrome 增加啟動參數(shù):
$ /path/to/chrome.app --disable-web-security
即可關(guān)閉属百。關(guān)閉后瀏覽器會有安全風(fēng)險记劝,建議只用在本地開發(fā)上
源由協(xié)議、域名(準(zhǔn)確來說是主機(jī)名族扰,因為除了域名也可以是 IP)厌丑、端口號共同決定,三者完全相同的兩個 URL 會被認(rèn)為是同源渔呵。舉幾個例子:
-
https://www.bytedance.com
和https://jobs.bytedance.com
怒竿,域名不同,不同源 -
http://www.bytedance.com
和https://www.bytedance.com
扩氢,協(xié)議不同耕驰,不同源 -
http://localhost
和http://localhost:8080
,端口不同(80 和 8080)录豺,不同源
特別的朦肘,當(dāng)我們直接打開一個 HTML 文件時饭弓,使用的是 file
協(xié)議,請求 HTTP 資源時協(xié)議不同媒抠,會產(chǎn)生跨域弟断。
源是允許被有限度的修改,瀏覽器提供了 API 可以將子域名下的源修改為父域名的源趴生,例如我們在 https://open.feishu.cn
下執(zhí)行:
document.domain = 'feishu.cn';
此時頁面的源就變成了 https://feishu.cn
滿足同源策略阀趴,我們就可以直接訪問父域名下的資源。如果修改為非父域名(如 bytedance.com
)苍匆,瀏覽器會報錯刘急。
同源策略控制不同源之間的操作,這些操作通常分為三類:
-
資源嵌入:一般是被允許的锉桑。例如
<script>
排霉、<iframe>
引入資源 - 寫操作:一般是被允許的。例如鏈接民轴、重定向以及表單提交攻柠,滿足特定條件的 HTTP 請求不允許
-
讀操作:一般是不被允許的。例如
XMLHttpRequest
和Fetch API
發(fā)起 GET 請求
根據(jù)這個分類后裸,我們來重點(diǎn)討論幾種情況:
-
<script>
是資源嵌入不受同源策略控制瑰钮,JSONP 就是借助這一特性實現(xiàn)的跨域 -
<form>
是資源寫入不受同源策略控制,因此表單的action
不是同源 URL 時也可以提交成功 -
XMLHttpRequest
或Fetch API
是我們重點(diǎn)關(guān)注的微驶,下面我們具體討論下:- 當(dāng)發(fā)起 HTTP
GET
請求(也可以是其它類型)讀取資源浪谴,受同源策略控制,請求返回的內(nèi)容在不同源的情況下我們是讀不到的 - 當(dāng)發(fā)起 HTTP
POST
請求修改資源因苹,一般是不受同源策略控制的(稱為簡單請求)苟耻,資源是允許被修改;而對于非簡單請求扶檐,受同源策略控制凶杖,資源不允許修改
- 當(dāng)發(fā)起 HTTP
需要特別說明的是:
- HTTP
GET
讀取資源雖然受同源策略控制,但請求是成功發(fā)送的款筑,只是瀏覽器限制了請求返回的內(nèi)容不給我們而是拋出了錯誤 - HTTP
POST
寫入資源時智蝠,簡單請求也是發(fā)送成功的,因此資源被成功修改了奈梳,因此可以說寫操作不受同源策略控制杈湾,但請求的返回值我們同樣獲取不到(因為是讀操作) - 非簡單請求采用了先發(fā)一個預(yù)檢請求的方式,判斷是否允許跨域攘须,不允許則不會發(fā)送真實的請求漆撞,避免資源被修改,因此受同源策略控制。下面實踐部分會詳細(xì)討論如何允許跨域叫挟。
-
WebSocket
不受同源策略控制
簡單請求必須滿足以下條件:
- HTTP 方法是:
GET
POST
或HEAD
- 除了被瀏覽器自動設(shè)置的字段(例如
Connection
艰匙、User-Agent
),請求頭只允許以下字段:Accept
Accept-Language
Content-Language
-
Content-Type
:只允許值為text/plain
multipart/form-data
application/x-www-form-urlencoded
-
Range
:只允許簡單的范圍標(biāo)頭值抹恳,如bytes=256-
或bytes=127-255
不滿足上述條件的即為非簡單請求员凝。例如當(dāng)我們在請求頭增加 X-JWT-Token
或 Content-Type: application/json
時,這個請求就是非簡單請求奋献。
為什么需要同源策略
上面我們說了健霹,同源策略是一個安全策略。如果同源策略被關(guān)閉瓶蚂,個人信息將會有安全風(fēng)險糖埋,例如:
- 惡意站點(diǎn)可以調(diào)用其它網(wǎng)站的用戶信息接口,獲取敏感信息
- 惡意站點(diǎn)可以調(diào)用其它網(wǎng)站的點(diǎn)贊或刪除接口窃这,惡意修改資源
跨域的最佳實踐
那么我們?nèi)绾尾拍芸缬蚰赝穑繛g覽器在同源策略的基礎(chǔ)上,提出一種可以安全的跨域機(jī)制稱為跨源資源共享(Cross Origin Resource Sharing杭攻,CORS)祟敛。當(dāng)然在這個機(jī)制發(fā)布之前,也有 JSONP
等滿足瀏覽器安全機(jī)制的跨域方案兆解,本文不具體討論馆铁。
CORS 的使用很簡單,服務(wù)端(即被請求的資源)在響應(yīng)頭中增加:
Access-Control-Allow-Origin: https://www.bytedance.com
即可锅睛,其中的值表示允許跨域訪問的源埠巨,此時 https://www.bytedance.com
就可以訪問這個服務(wù)器的資源。我們可以看出现拒,請求是成功發(fā)送并得到響應(yīng)的辣垒,瀏覽器才能拿到 Access-Control-Allow-Origin
響應(yīng)頭并判斷是否可以跨域(先請求再判斷)。那么對于非簡單請求印蔬,為了避免資源被意外修改乍构,是需要先判斷是否可以跨域再發(fā)起修改請求的(先判斷再請求),這時瀏覽器就會先發(fā)起一個預(yù)檢請求(HTTP 方法為 OPTIONS
)扛点,拿到服務(wù)端返回的 Access-Control-Allow-Origin
并判斷,滿足允許跨域的話再發(fā)起真實請求岂丘。
如果我們無法修改服務(wù)端呢陵究?可以實現(xiàn)一個我們可控的代理服務(wù)端,增加允許跨域響應(yīng)頭或直接同域奥帘,解決跨域問題铜邮,例如通過 Nginx
、Charles
或 webpack-dev-server
代理。
下面我們來討論項目中幾種常見的跨域場景和最佳實踐:
跨域請求需要攜帶 Cookie
XMLHttpRequest
和 Fetch API
在發(fā)起跨域請求時默認(rèn)是不攜帶服務(wù)端所在源的 Cookie松蒜,這樣影響到了服務(wù)端的用戶身份鑒別扔茅。我們需要增加參數(shù),例如:
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
或
fetch(url, {
credentials: 'include',
});
瀏覽器才會攜帶 Cookie秸苗,同時服務(wù)端需要返回響應(yīng)頭:
Access-Control-Allow-Credentials: true
瀏覽器才會正常將響應(yīng)內(nèi)容返回給我們召娜,否則會拋出錯誤
允許白名單內(nèi)的源跨域請求
當(dāng)有多個源需要允許跨域訪問時,服務(wù)端可以配置
Access-Control-Allow-Origin: *
允許所有源跨域訪問惊楼,但開放范圍太大有一定的安全隱患玖瘸,同時這種情況下瀏覽器不允許攜帶 Cookie。我們需要對源精細(xì)化的控制檀咙,但 Access-Control-Allow-Origin
不允許設(shè)置多個源雅倒。我們可以通過請求頭 Origin
加白名單判斷的方式,動態(tài)返回 Access-Control-Allow-Origin
的值解決弧可。
Origin
是跨域請求時瀏覽器自動攜帶的值蔑匣,表示請求的源,我們在服務(wù)端定義一個白名單判斷這個源是否在白名單中棕诵,如果在則返回 Access-Control-Allow-Origin
的值等于請求頭的 Origin
裁良。以 express
為例代碼如下:
const whiteList = ['https://jobs.bytedance.com', 'https://www.bytedance.com'];
app.get('/path/to/api', (request, response) => {
const { origin } = request.headers;
if (whiteList.includes(origin)) {
response.header('Access-Control-Allow-Origin', origin);
response.header('Access-Control-Allow-Credentials', true);
}
});
獲取跨域請求返回的某些響應(yīng)頭
跨域響應(yīng)中的響應(yīng)頭默認(rèn)并不是所有的都可以被我們獲取到,默認(rèn)瀏覽器只返回一些基本響應(yīng)頭年鸳,包括:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
當(dāng)我們需要獲取一些額外的響應(yīng)頭趴久,例如 X-TT-LogID
用于記錄每次請求的 logID
,我們需要讓服務(wù)端返回響應(yīng)頭:
Access-Control-Expose-Headers: X-TT-LogID
才可以獲取到
獲取資源使用簡單請求
前面我們了解到搔确,非簡單請求會先發(fā)起預(yù)檢請求以檢查是否允許跨域彼棍,因此需要兩次 HTTP 請求才能完成這次操作。對于我們已知是讀操作的請求膳算,我們可以盡量滿足簡單請求的條件以減少預(yù)檢請求座硕,從而減少請求時間。
一般來說涕蜂,我們需要注意的地方如下:
- 讀取資源時盡量使用
GET
或POST
請求 - 對于
POST
請求且需要通過 body 傳遞 JSON 數(shù)據(jù)時华匾,Content-Type
使用text/plain
而不是application/json
,同時服務(wù)端也需要支持將text/plain
解析成 JSON - 對于其它需要請求攜帶的信息机隙,盡量放在請求參數(shù)中而不是自定義請求頭蜘拉,因為額外的請求頭也會導(dǎo)致變成非簡單請求,例如 JWT 的
X-JWT-Token
CDN 資源也需要允許跨域
如果我們接入了 Sentry
或 Perfsee
等前端監(jiān)控平臺并需要監(jiān)控站點(diǎn)的 JS 腳本錯誤有鹿,且這些 JS 腳本是部署在 CDN 服務(wù)器上旭旭,由于 CDN 和我們的站點(diǎn)往往不同源,瀏覽器對于不同源的 <script>
資源嵌入不會返回具體的錯誤信息(讀取錯誤信息屬于讀操作受同源策略控制)葱跋,這樣監(jiān)控平臺收集到的錯誤信息不完整持寄,對定位問題帶來了負(fù)面影響源梭。
我們需要讓 CDN 服務(wù)器也返回 Access-Control-Allow-Origin
頭允許我們的源,同時所有引入 JS 的 <script>
需要增加 crossorigin
屬性:
<script src="/path/to/cdn" crossorigin="anonymous"></script>
總結(jié)
我們從三個角度討論了跨域是什么稍味、為什么需要同源策略以及跨域的解決方案和實踐废麻,重點(diǎn)總結(jié)如下:
- 同源策略是瀏覽器的安全策略,Node.js 環(huán)境沒有跨域限制
- 瀏覽器的同源策略通過啟動參數(shù)
disable-web-security
可以關(guān)閉 - 通過協(xié)議模庐、域名烛愧、端口號三元組判斷同源
- 通過
document.domain
可以有限度的修改源 - 簡單請求與非簡單請求的區(qū)別,非簡單請求會先發(fā)起預(yù)檢請求
- 同源策略保證了瀏覽器的安全性
- 跨域通過增加響應(yīng)頭
Access-Control-Allow-Origin
解決赖欣,也可以借助代理實現(xiàn) - 跨域的一些最佳實踐:
- 跨域請求通過
Access-Control-Allow-Credentials
攜帶 Cookie - 通過請求頭
Origin
允許白名單內(nèi)的源跨域請求 - 通過
Access-Control-Expose-Headers
獲取跨域請求返回的某些響應(yīng)頭 - 獲取資源盡量使用簡單請求屑彻,以減少請求耗時
- CDN 資源也需要允許跨域
- 跨域請求通過
如果有其它關(guān)于跨域的最佳實踐,歡迎分享