【面試必會(huì)】煩不勝煩的跨域問題?再見
最近接了個(gè)外包項(xiàng)目,我負(fù)責(zé)后端,想著省事直接用 Django,吭哧吭哧寫了一天 API 后,拉了前端代碼下來聯(lián)調(diào),果不其然遇到了跨域問題。然而這個(gè)跨域有點(diǎn)詭異,折騰了一天時(shí)間,頂不住睡一覺,第二天一起床反而解決了。這個(gè)故事告訴我們,做到十二點(diǎn),就不做了,睡大覺!
談到跨域,就不可避免的要講講CORS,這些到底都是啥?它們有什么區(qū)別?請聽我慢慢道來。
跨域資源共享
CORS (Cross-Origin Resource Sharing,跨域資源共享)是一個(gè)系統(tǒng),它由一系列傳輸?shù)?HTTP頭組成,這些HTTP頭決定瀏覽器是否阻止前端 JavaScript 代碼獲取跨域請求的響應(yīng)。
簡而言之,在前后端分離的開發(fā)模式下,我們的前端在向后臺(tái)請求資源時(shí),由于瀏覽器的同源策略,默認(rèn)情況下我們是無法獲得資源的,這個(gè)時(shí)候,就會(huì)產(chǎn)生我們常說的跨域問題:

如上圖所示,在瀏覽器的Console控制臺(tái)中,當(dāng)我們發(fā)送的請求遇到跨域問題時(shí),就會(huì)出現(xiàn)上圖的報(bào)錯(cuò)信息。
瀏覽器的同源策略
在了解跨域問題的解決辦法之前,我們先來看看瀏覽器的同源策略是怎么一回事:
如下圖所示,Web document我們可以理解為前端,所在域?yàn)?code style="font-size: inherit;line-height: inherit;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">domain-a.com;當(dāng)它請求同域名下的Web server(即左上方的服務(wù)器)時(shí),它們的請求是同源請求(總被允許的),而當(dāng)它請求domain-b.com下的Web server時(shí),它們的請求是跨域請求(受 CORS 控制)。

也就是說,我們的跨域問題本質(zhì)上不是一個(gè)問題,而是在跨域請求中,我們沒有用CORS控制我們的服務(wù)器允許該請求,那么解決辦法也就很簡單了,我們需要修改后臺(tái)的一些配置,使其允許來自前端的跨域請求。
我們前面也提到,CORS 由一系列的傳輸 HTTP 頭組成,實(shí)際上,跨域資源共享標(biāo)準(zhǔn)新增了一組 HTTP 首部字段,允許服務(wù)器聲明哪些源站通過瀏覽器有權(quán)限訪問哪些資源。
綜合來看,實(shí)際上我們要修改的配置,就是返回給前端的response上的頭(headers)。
回到故事的開始
稍微查閱資料,我們在response的頭中加入:Access-Control-Allow-Origin: *;但事情并沒有這么簡單,在我的 Django 后臺(tái)中,我嘗試了自定義middleware、使用第三方的django-cors-headers、直接修改response返回值,發(fā)現(xiàn)居然都不起作用。翻遍全網(wǎng),來來去去都是這幾個(gè)辦法,迫于無奈,我打斷點(diǎn),Debug,甚至還瞎改了一通django-cors-headers的源碼,驗(yàn)證一些想法,最后發(fā)現(xiàn)的原因:前端的鍋。
注意下圖中注釋掉的部分,當(dāng)我將其注釋掉后,后端使用django-cors-headers的情況下,跨域問題就解決了,那么為什么加上注釋中的部分就會(huì)導(dǎo)致跨域問題呢?

這里我們不得不再深究一下跨域的相關(guān)知識(shí),否則每次遇到,都只能找各種或斷斷續(xù)續(xù)、或版本不對的博客去看,問題沒解決,時(shí)間倒是浪費(fèi)了。
深究跨域相關(guān)知識(shí)
CORS規(guī)范要求,對那些可能對服務(wù)器數(shù)據(jù)產(chǎn)生副作用的 HTTP 請求方法(特別是GET以外的 HTTP 請求,或者搭配某些 MIME 類型的POST請求),瀏覽器必須首先使用OPTIONS方法發(fā)起一個(gè)預(yù)檢請求(preflight request),從而獲知服務(wù)端是否允許該跨域請求。服務(wù)器確認(rèn)允許之后,才發(fā)起實(shí)際的 HTTP 請求。
從上述內(nèi)容中我們可以了解到,CORS僅針對部分請求會(huì)先發(fā)起一個(gè)預(yù)檢請求(OPTIONS),而對于其他的簡單請求則不需要這個(gè)過程。
我們先來看看哪些情況是符合簡單請求的:
簡單請求
在本文指代:某些不會(huì)觸發(fā) CORS 預(yù)檢請求的請求。
使用列出的三種方法之一:GET、POST、HEAD
Fetch 規(guī)范定義了
對 CORS 安全的首部字段集合,不得人為設(shè)置該集合之外的其他首部字段。Accept
Accept-Language
Content-Language
Content-Type(注意,第三點(diǎn)為對該字段的限制)DPR
Downlink
Save-Data
Viewport-Width
Width
Content-Type的值僅限三種之一text/plain
multipart/form-data
application/x-www-form-urlencoded
請求中的任意
XMLHttpRequestUpload對象均沒有注冊任何事件監(jiān)聽器;XMLHttpRequestUpload對象可以使用XMLHttpRequest.upload屬性訪問。請求中沒有使用
ReadableStream對象。
需要滿足上述五個(gè)條件,才符合簡單請求的要求。我們?nèi)绻軐㈩A(yù)檢請求降為簡單請求,那么就能解決跨域問題。
如何解決呢?對于簡單請求,我們只需要在返回的response中加入Access-Control-Allow-Origin控制頭,把前端所在域加入或者直接使用*,即可解決。

而回到故事中,前端發(fā)起的login請求屬于POST請求,Content-Type也是application/x-www-form-urlencoded符合要求,問題就出在這個(gè)headers上,這個(gè)自定義headers使得第二點(diǎn)不滿足,所以瀏覽器會(huì)先發(fā)起一個(gè)預(yù)檢請求,不同于簡單請求,包含預(yù)檢請求的處理方式較為麻煩,直接在response中加入控制頭是不足以解決跨域問題的。

我們再來看看詳細(xì)的預(yù)檢請求:
預(yù)檢請求
與前述簡單請求不同,需預(yù)檢的請求要求必須首先使用 OPTIONS方法發(fā)起一個(gè)預(yù)檢請求到服務(wù)器,以獲知服務(wù)器是否允許該實(shí)際請求。預(yù)檢請求的使用,可以避免跨域請求對服務(wù)器的用戶數(shù)據(jù)產(chǎn)生未預(yù)期的影響。
不滿足簡單請求的就是預(yù)檢請求,比如:PUT、DELETE請求等等。
如下圖所示,這是一個(gè)預(yù)檢請求的例子,我們的POST請求中包含了自定義的headers:X_PINGOTHER: pingpong,因此先發(fā)送了一個(gè)OPTIONS請求:
在得到的response中,我們可以看到:
Access-Control-Allow-Origin中有Client的來源origin:http://foo.example;
Access-Control-Allow-Methods中有POST;
Access-Control-Allow-Headers中也有自定義的X-PINGOTHER;
因此Client得到允許,繼續(xù)向Server發(fā)送POST請求,最終獲得數(shù)據(jù)。
另外,首部字段 Access-Control-Max-Age 表明該響應(yīng)的有效時(shí)間為 86400 秒,也就是 24 小時(shí)。在有效時(shí)間內(nèi),瀏覽器無須為同一請求再次發(fā)起預(yù)檢請求。請注意,瀏覽器自身維護(hù)了一個(gè)最大有效時(shí)間,如果該首部字段的值超過了最大有效時(shí)間,將不會(huì)生效。

回到故事,在此時(shí)我進(jìn)行了一個(gè)小實(shí)驗(yàn),我把前端中的headers的注釋取消掉,

在后端的CORS控制中,在Access-Control-Allow-Headers中加入:

此時(shí)我們重新測試請求,發(fā)現(xiàn)解決了跨域問題。
總結(jié)
在這里,我們先總結(jié)一下如何解決跨域問題:
對于簡單請求,直接操作對應(yīng)的
response的headers,一般Access-Control-Allow-Origin和Access-Control-Allow-Methods就能夠解決;對于預(yù)檢請求,一般還需要Access-Control-Allow-Headers。【如果需要傳輸cookie,那么會(huì)涉及:Access-Control-Allow-Credentials】檢查當(dāng)前的請求是簡單請求還是預(yù)檢請求,如果不是簡單請求,能否變?yōu)楹唵握埱螅?/p>
如果當(dāng)前是預(yù)檢請求,那么在
Chrome中使用F12->Network中檢查Request Headers,檢查是否有簡單請求中不允許的頭,記錄下來。下圖是我用另一個(gè)項(xiàng)目的前端來測試這個(gè)后端的跨域問題是否解決,在該前端項(xiàng)目中使用了一個(gè)第三方的
Encoding-Type控制頭:

我們在后端的CORS控制中加入,成功解決。

在后端服務(wù)中,我們往往會(huì)使用一些現(xiàn)成的CORS處理插件,比如Django中的django-cors-headers,那么只需按照其github頁面的配置方式,同時(shí)注意上述的自定義頭的控制,那么處理跨域問題非常簡單;如果不是使用現(xiàn)成的CORS處理插件,我們就需要對OPTIONS請求和各種請求的Response進(jìn)行處理,最好是統(tǒng)一處理,比如在Springboot中可以在AOP層統(tǒng)一處理;
在更輕量的情況下,我們也可以直接對單一請求的response進(jìn)行處理:
func?login(w?http.ResponseWriter,?r?*http.Request)?{
????w.Header().Set("Access-Control-Allow-Origin",?"*")?????????????//允許訪問所有域
????w.Header().Add("Access-Control-Allow-Headers",?"Content-Type")?//header的類型
????w.Header().Set("content-type",?"application/json")?//返回?cái)?shù)據(jù)格式是json
????resp?:=?`{"code":?"00",
??????????????"message":?"SUCCESS",
??????????????"describe":?"登錄成功"
?????????????}`
????type?JsonResp?struct?{
????????Code???????int???????????????`json:"code"`
????????Message????string????????????`json:"message"`
????????Describe???string????????????`json:"describe"`
????}
????var?smsresp?JsonResp
????temp?:=?[]byte(resp)
????json.Unmarshal(temp,?&smsresp)
????fmt.Fprintf(w,?string(temp))
}
上面是我之前用go語言編寫的一個(gè)loginAPI,我們在response中加入Access-Control-Allow-Origin用以解決跨域問題,非常直觀。
文章整體目錄

如何獲取
很簡單,在我的微信公眾號(hào)?帥地玩編程?回復(fù)?程序員內(nèi)功修煉?即可獲取《程序員內(nèi)功修煉》第一版和第二版的 PDF。
推薦,推薦一個(gè) GitHub,這個(gè) GitHub 整理了幾百本常用技術(shù)PDF,絕大部分核心的技術(shù)書籍都可以在這里找到,GitHub地址:https://github.com/iamshuaidi/CS-Book(電腦打開體驗(yàn)更好),地址閱讀原文直達(dá)
