深入理解跨域和最佳實踐分享

跨域是前端開發(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.comhttps://jobs.bytedance.com怒竿,域名不同,不同源
  • http://www.bytedance.comhttps://www.bytedance.com扩氢,協(xié)議不同耕驰,不同源
  • http://localhosthttp://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 請求不允許
  • 讀操作:一般是不被允許的。例如 XMLHttpRequestFetch API 發(fā)起 GET 請求

根據(jù)這個分類后裸,我們來重點(diǎn)討論幾種情況:

  • <script> 是資源嵌入不受同源策略控制瑰钮,JSONP 就是借助這一特性實現(xiàn)的跨域
  • <form> 是資源寫入不受同源策略控制,因此表單的 action 不是同源 URL 時也可以提交成功
  • XMLHttpRequestFetch API 是我們重點(diǎn)關(guān)注的微驶,下面我們具體討論下:
    • 當(dāng)發(fā)起 HTTP GET 請求(也可以是其它類型)讀取資源浪谴,受同源策略控制,請求返回的內(nèi)容在不同源的情況下我們是讀不到的
    • 當(dāng)發(fā)起 HTTP POST 請求修改資源因苹,一般是不受同源策略控制的(稱為簡單請求)苟耻,資源是允許被修改;而對于非簡單請求扶檐,受同源策略控制凶杖,資源不允許修改

需要特別說明的是:

  • HTTP GET 讀取資源雖然受同源策略控制,但請求是成功發(fā)送的款筑,只是瀏覽器限制了請求返回的內(nèi)容不給我們而是拋出了錯誤
  • HTTP POST 寫入資源時智蝠,簡單請求也是發(fā)送成功的,因此資源被成功修改了奈梳,因此可以說寫操作不受同源策略控制杈湾,但請求的返回值我們同樣獲取不到(因為是讀操作)
  • 非簡單請求采用了先發(fā)一個預(yù)檢請求的方式,判斷是否允許跨域攘须,不允許則不會發(fā)送真實的請求漆撞,避免資源被修改,因此受同源策略控制。下面實踐部分會詳細(xì)討論如何允許跨域叫挟。
  • WebSocket 不受同源策略控制

簡單請求必須滿足以下條件:

  • HTTP 方法是:GET POSTHEAD
  • 除了被瀏覽器自動設(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-TokenContent-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)頭或直接同域奥帘,解決跨域問題铜邮,例如通過 NginxCharleswebpack-dev-server 代理。

下面我們來討論項目中幾種常見的跨域場景和最佳實踐:

跨域請求需要攜帶 Cookie

XMLHttpRequestFetch 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ù)檢請求座硕,從而減少請求時間。

一般來說涕蜂,我們需要注意的地方如下:

  • 讀取資源時盡量使用 GETPOST 請求
  • 對于 POST 請求且需要通過 body 傳遞 JSON 數(shù)據(jù)時华匾,Content-Type 使用 text/plain 而不是 application/json,同時服務(wù)端也需要支持將 text/plain 解析成 JSON
  • 對于其它需要請求攜帶的信息机隙,盡量放在請求參數(shù)中而不是自定義請求頭蜘拉,因為額外的請求頭也會導(dǎo)致變成非簡單請求,例如 JWT 的 X-JWT-Token

CDN 資源也需要允許跨域

如果我們接入了 SentryPerfsee 等前端監(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)于跨域的最佳實踐,歡迎分享

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末顶吮,一起剝皮案震驚了整個濱河市社牲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌悴了,老刑警劉巖搏恤,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異湃交,居然都是意外死亡熟空,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門搞莺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來息罗,“玉大人,你說我怎么就攤上這事才沧÷鹾恚” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵温圆,是天一觀的道長挨摸。 經(jīng)常有香客問我,道長岁歉,這世上最難降的妖魔是什么得运? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮锅移,結(jié)果婚禮上熔掺,老公的妹妹穿的比我還像新娘。我一直安慰自己非剃,他們只是感情好瞬女,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著努潘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疯坤,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天报慕,我揣著相機(jī)與錄音,去河邊找鬼压怠。 笑死眠冈,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的菌瘫。 我是一名探鬼主播蜗顽,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼雨让!你這毒婦竟也來了雇盖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤栖忠,失蹤者是張志新(化名)和其女友劉穎崔挖,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庵寞,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狸相,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了捐川。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脓鹃。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖古沥,靈堂內(nèi)的尸體忽然破棺而出瘸右,到底是詐尸還是另有隱情,我是刑警寧澤渐白,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布尊浓,位于F島的核電站,受9級特大地震影響纯衍,放射性物質(zhì)發(fā)生泄漏栋齿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一襟诸、第九天 我趴在偏房一處隱蔽的房頂上張望瓦堵。 院中可真熱鬧,春花似錦歌亲、人聲如沸菇用。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惋鸥。三九已至杂穷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卦绣,已是汗流浹背耐量。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留滤港,地道東北人廊蜒。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像溅漾,于是被迫代替她去往敵國和親山叮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容