作為前端開(kāi)發(fā),從入行起,應(yīng)該就接觸過(guò)跨域的概念。工作中,在與服務(wù)端配合時(shí),也經(jīng)常需要處理跨域相關(guān)問(wèn)題。如果你不能理解到底什么是跨域,那在與服務(wù)端配合解決跨域問(wèn)題時(shí),你可能就要落入對(duì)方的掌控之中了(哈哈,開(kāi)個(gè)玩笑)。
為了避免這一尷尬的境地,今天我來(lái)帶著大家一起重溫并鞏固一下以下內(nèi)容:什么是跨域,跨域有哪些實(shí)際的開(kāi)發(fā)場(chǎng)景,有哪些方式可以快速的處理跨域問(wèn)題。
整篇內(nèi)容沒(méi)有涵蓋網(wǎng)上所有的與跨域“沾邊”的知識(shí),而是從實(shí)際場(chǎng)景出發(fā),旨在解決工作中經(jīng)常遇到的跨域問(wèn)題。
一、什么是跨域?
1.1 同源策略 VS 跨域
如果讓你直接定義什么是跨域,你可能會(huì)發(fā)現(xiàn)很難定義。所以需要借助與之對(duì)立的概念——同源策略(SOP,Same-Origin Policy)。只要不是同源的,那就是跨域的。
同源策略是瀏覽器中一個(gè)極為至關(guān)重要的安全機(jī)制,可以用來(lái)限制某一源內(nèi)的文檔或腳本與另一源內(nèi)的資源如何進(jìn)行交互。其目的在于隔離潛在的惡意文檔,減少可能存在的攻擊。
而對(duì)于同源的定義是:協(xié)議、主機(jī)名(host)以及端口三者均相同。
維基百科中對(duì)于URI的結(jié)構(gòu)組成說(shuō)明如下:
[協(xié)議名]://[用戶名]:[密碼]@[主機(jī)名]:[端口]/[路徑]?[查詢(xún)參數(shù)]#[片段ID]
常規(guī)情況下,我們無(wú)需在URI中帶有用戶名密碼等信息用于驗(yàn)證。這只是一個(gè)完整的URI組成示例。
所以對(duì)于同源,只要URI中協(xié)議名、主機(jī)名、端口三者有其中一條不同,則視為不同源。不同源之間請(qǐng)求資源,則為跨域。其中主機(jī)名部分,主域和子域視為不同、域名與其對(duì)應(yīng)的IP也視為不同,這就是說(shuō)看著必須得一樣。
1.2 跨域的限制
當(dāng)存在跨域問(wèn)題時(shí),瀏覽器會(huì)做出一定的限制措施。主要包括以下三點(diǎn):
- Cookie、LocalStorage 和 IndexDB 無(wú)法讀取。
- DOM 無(wú)法獲得。
- AJAX 請(qǐng)求被攔截。
注:Cookie獲取不檢測(cè)端口
二、常見(jiàn)的跨域開(kāi)發(fā)場(chǎng)景/業(yè)務(wù)場(chǎng)景
撇開(kāi)場(chǎng)景談概念,一定是晦澀難懂的,開(kāi)始說(shuō)了,本文旨在解決實(shí)際工作中遇到的跨域問(wèn)題。下面我們來(lái)一起看看工作過(guò)程中比較常見(jiàn)的跨域場(chǎng)景。
2.1 前后端分離:純前端 + 接口層 (開(kāi)發(fā)模式)
在前后端分離的開(kāi)發(fā)模式下,開(kāi)發(fā)環(huán)境應(yīng)該用webpack的居多(當(dāng)然有的可能不是,以此為例),與之相應(yīng)的web服務(wù)器就是webpack-dev-server。這類(lèi)開(kāi)發(fā)模式的架構(gòu)一般如下:
這一架構(gòu)下,dev-server中的頁(yè)面如果通過(guò)ajax直接調(diào)用服務(wù)端的API會(huì)存在跨域問(wèn)題。
2.2 前后端分離:純前端 + 接口層 (生產(chǎn)模式)
與第一種方式相似,前后端分離的項(xiàng)目在開(kāi)發(fā)完成后,往往通過(guò)nginx等作為靜態(tài)資源服務(wù)器,前端頁(yè)面直接通過(guò)ajax發(fā)送請(qǐng)求,依然存在跨域請(qǐng)求問(wèn)題。
架構(gòu)如下:
2.3 前后端分離:純前端 + BFF + 接口層
有時(shí)候,我們所要調(diào)用的接口層可能并不只是給我們提供服務(wù),他們只會(huì)提供一些通用的數(shù)據(jù),我們需要對(duì)數(shù)據(jù)進(jìn)行一定程度的二次加工;也可能我們需要自己給前端頁(yè)面提供一些通用的功能,如圖片上傳等。這時(shí),就需要在前端頁(yè)面和接口層之間增加一個(gè)BFF層(Backends For Frontends)。
BFF層一般由前端維護(hù),所以使用Node.js居多。
這一架構(gòu)如下:
使用這種架構(gòu)其實(shí)本身已經(jīng)解決了跨域問(wèn)題,是一種跨域解決方式,后面我們?cè)偌?xì)說(shuō)。
2.4 服務(wù)端渲染 + web服務(wù)器(不跨域)
最后一種是最原始的web服務(wù)架構(gòu),html頁(yè)面以及其他靜態(tài)資源都直接從服務(wù)器獲取,接口也直接由所在服務(wù)器處理。這種方式不存在跨域問(wèn)題。前端和服務(wù)端邏輯完全綁定,互相支撐提供服務(wù)。
三、跨域的解決方式
前面我們提到,跨域是瀏覽器的限制。所以我們想解決跨域問(wèn)題可以有兩個(gè)方向,第一是繞開(kāi)瀏覽器限制,第二是通過(guò)瀏覽器支持的方式來(lái)允許跨域。
下面我們分別會(huì)介紹三種繞開(kāi)瀏覽器限制的解決方式,分別為webpack-dev-server代理/Nginx代理轉(zhuǎn)發(fā)/服務(wù)器代理,以及瀏覽器本身支持的CORS方式。
沒(méi)有大家耳熟能詳?shù)腏SONP,大家自行科普一下吧。
3.1 webpack-dev-server代理
對(duì)于上面說(shuō)到的“前后端分離:純前端 + 接口層 (開(kāi)發(fā)模式)”這一場(chǎng)景,當(dāng)我們?cè)?code>http://co.com的頁(yè)面上直接調(diào)用http://api.co.com的接口時(shí),會(huì)出現(xiàn)跨域問(wèn)題。
我們可以將所有的接口請(qǐng)求都從http://co.com發(fā)出,如http://co.com/api/getSomeData(額外加了/api,方便統(tǒng)一轉(zhuǎn)發(fā)),最后通過(guò)proxy配置代理,轉(zhuǎn)發(fā)到最終的接口服務(wù)器http://api.co.com/getSomeData。
proxy配置如下:
devServer: {
proxy: {
'/api': {
target: 'http://api.co.com',
// 如果轉(zhuǎn)發(fā)后的pathname需要改變,可以通過(guò)以下方式重寫(xiě)
// 下面是把a(bǔ)pi前綴去掉
pathRewrite: {
'^/api/': '',
},
},
}
}
通過(guò)上述方式,我們可以在接口請(qǐng)求發(fā)起的時(shí)候,統(tǒng)一從當(dāng)前所在源發(fā)起,最后通過(guò)proxy代理的方式轉(zhuǎn)發(fā)到真正的接口層。這樣就繞開(kāi)了調(diào)用接口時(shí)瀏覽器的同源限制。
3.2 Nginx代理轉(zhuǎn)發(fā)
針對(duì)第二部分提到的“前后端分離:純前端 + 接口層(生產(chǎn)模式)”這一場(chǎng)景,這時(shí)我們沒(méi)有webpack-dev-server可用了,不過(guò)沒(méi)關(guān)系,我們?cè)谑褂胣ginx作為靜態(tài)資源服務(wù)器時(shí),也可以做一些代理轉(zhuǎn)發(fā)??梢詫⒔涌谡?qǐng)求全部轉(zhuǎn)發(fā)到對(duì)應(yīng)接口服務(wù)器。
配置如下:
location /api {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
# 轉(zhuǎn)發(fā)時(shí)重寫(xiě)地址
rewrite ^/api/(.*)$ /$1 break;
# 轉(zhuǎn)發(fā)目的地
proxy_pass http://api.co.com;
}
這種方式其實(shí)與第一種類(lèi)似,只不過(guò)是通過(guò)不同的方式進(jìn)行代理。這種方式也是通過(guò)繞過(guò)瀏覽器限制的方式解決跨域的。
3.3 服務(wù)器轉(zhuǎn)發(fā)
第二部分的第三種架構(gòu)“前后端分離:純前端 + BFF + 接口層”,這種架構(gòu)其實(shí)就已經(jīng)解決了跨域的問(wèn)題,前端頁(yè)面的所有接口都由BFF層進(jìn)行管理。
對(duì)于BFF層,可以通過(guò)添加中間件或者其他的方式對(duì)于接口進(jìn)行攔截。如果是靜態(tài)資源或者是當(dāng)前服務(wù)所提供的接口,則直接處理。如果是調(diào)用api的接口請(qǐng)求,將其轉(zhuǎn)發(fā)到對(duì)應(yīng)的服務(wù)即可。
不同的框架有不同的方式來(lái)處理接口攔截與轉(zhuǎn)發(fā),所以此處沒(méi)有代碼。
3.4 CORS 跨域資源共享
跨域資源共享(CORS) 是一種機(jī)制,服務(wù)端可以通過(guò)額外的 HTTP 頭來(lái)告訴瀏覽器允許某一源內(nèi)的Web應(yīng)用訪問(wèn)不同源服務(wù)器上的指定資源。
CORS使用通用的跨域解決方式,需要服務(wù)端配合進(jìn)行實(shí)現(xiàn)。
這里面會(huì)涉及到簡(jiǎn)單請(qǐng)求以及預(yù)檢請(qǐng)求的概念。關(guān)于什么是簡(jiǎn)單請(qǐng)求,大家可以移步MDN看下詳細(xì)的定義,這里不再詳述了。
這兩種請(qǐng)求的區(qū)別在于,對(duì)于預(yù)檢請(qǐng)求,瀏覽器必須首先使用OPTIONS方法發(fā)起一個(gè)預(yù)檢請(qǐng)求(preflight request),從而獲知服務(wù)端是否允許該跨域請(qǐng)求。服務(wù)器確認(rèn)允許之后,才發(fā)起實(shí)際的HTTP請(qǐng)求。
- 為什么要區(qū)分簡(jiǎn)單請(qǐng)求和預(yù)檢請(qǐng)求可以參考賀老的這篇文章
- IE8/9不支持CORS,通過(guò)XDomainRequest來(lái)實(shí)現(xiàn)
3.4.1 簡(jiǎn)單請(qǐng)求
對(duì)于簡(jiǎn)單請(qǐng)求,服務(wù)端通過(guò)簡(jiǎn)單的設(shè)置Access-Control-Allow-Origin: *即可允許任意來(lái)源進(jìn)行跨域請(qǐng)求。如果只想允許來(lái)自http://co.com的訪問(wèn),可以設(shè)置Access-Control-Allow-Origin: http://co.com。
通信過(guò)程示意圖如下:
注:在發(fā)起跨域請(qǐng)求時(shí),瀏覽器會(huì)在請(qǐng)求頭字段中自動(dòng)帶上Origin字段,值為當(dāng)前所在域。
3.4.2 預(yù)檢請(qǐng)求
對(duì)于預(yù)檢請(qǐng)求,服務(wù)端需要額外再多做一些事情。如下步驟:
- 首先,發(fā)起預(yù)檢請(qǐng)求,帶上真實(shí)請(qǐng)求的Method。
- 服務(wù)端判斷是否允許跨域請(qǐng)求,如果允許則返回允許的來(lái)源、允許的請(qǐng)求Methods以及預(yù)檢請(qǐng)求的有效時(shí)長(zhǎng)(有效時(shí)間內(nèi),同一請(qǐng)求無(wú)需再次發(fā)送預(yù)檢請(qǐng)求,不過(guò)不可以任意設(shè)置,瀏覽器有最大時(shí)長(zhǎng)限制)。
- 客戶端發(fā)起真實(shí)的跨域請(qǐng)求。
- 服務(wù)端返回。
通信過(guò)程示意圖如下:
需要注意的是,服務(wù)端在處理預(yù)檢請(qǐng)求時(shí),如果允許跨域,服務(wù)端只需要設(shè)置對(duì)應(yīng)的響應(yīng)頭,然后直接返回即可,無(wú)需其他處理。
3.4.3 附帶身份憑證的請(qǐng)求
常規(guī)來(lái)說(shuō),我們的請(qǐng)求都需要帶有身份憑證(如Cookie),這時(shí)服務(wù)器端的響應(yīng)中需要額外設(shè)置Access-Control-Allow-Credentials: true,如果未設(shè)置,瀏覽器將不會(huì)把響應(yīng)內(nèi)容返回給請(qǐng)求的發(fā)送者。
還有個(gè)別不是很常用的請(qǐng)求頭和響應(yīng)頭字段,大家可前往MDN查看完整的列表。
總結(jié)
如上,我們從以下三個(gè)方面介紹了跨域:什么是跨域,跨域有哪些實(shí)際的開(kāi)發(fā)場(chǎng)景,有哪些方式可以快速的處理跨域問(wèn)題。
大家在遇到跨域問(wèn)題時(shí),可以根據(jù)具體的場(chǎng)景選擇繞過(guò)跨域問(wèn)題,還是選擇通用的CORS模式來(lái)解決。
最后希望大家看完這篇文章之后,都會(huì)是『那些年我們“跨”過(guò)的“域”(接口篇)』。而不是『那些年我們都沒(méi)“跨”過(guò)去的“域”』?? ?? ??。