瀏覽器中的跨域問題與 CORS
?Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
?
?什么是跨域?[1]
?
跨域,這或許是前端面試中最常碰到的問題了,大概因?yàn)榭缬騿栴}是瀏覽器環(huán)境中的特有問題,而且隨處可見,如同蚊子不僅盯你肉而且處處圍著你轉(zhuǎn)讓你心煩。「你看,在服務(wù)器發(fā)起 HTTP 請求就不會有跨域問題的」。
當(dāng)談到跨域問題的解決方案時(shí),最流行也最簡單的當(dāng)屬 CORS 了。
CORS
CORS 即跨域資源共享 (Cross-Origin Resource Sharing, CORS)。簡而言之,就是在服務(wù)器端的響應(yīng)中加入幾個(gè)標(biāo)頭,使得瀏覽器能夠跨域訪問資源。
這個(gè)響應(yīng)頭的字段設(shè)置就是?Access-Control-Allow-Origin: *
以下是最簡單的一個(gè) CORS 請求
GET / HTTP/1.1
Host: shanyue.tech
Origin: http://shanyue.tech
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: text/plain; charset=utf-8
Content-Length: 12
Date: Wed, 08 Jul 2020 17:03:44 GMT
Connection: keep-alive
預(yù)請求與 Options
當(dāng)一個(gè)請求跨域且不是簡單請求時(shí)就會發(fā)起預(yù)請求,也就是?Options。如果沒有預(yù)請求,萬一有一個(gè)毀滅性的 POST 跨域請求直接執(zhí)行,雖然最后告知瀏覽器你沒有跨域權(quán)限,但是損失已造成,豈不虧大的。
以下條件構(gòu)成了簡單請求:
Method: 請求的方法是?GET、POST?及?HEADHeader: 請求頭是?Content-Type?(有限制)、Accept-Language、Content-Language?等Content-Type: 請求類型是?application/x-www-form-urlencoded、multipart/form-data?或?text/plain
非簡單請求一般需要開發(fā)者主動構(gòu)造,在項(xiàng)目中常見的?Content-Type: application/json?及?Authorization: ?為典型的「非簡單請求」。與之有關(guān)的三個(gè)字段如下:
Access-Control-Allow-Methods: 請求所允許的方法,?「用于預(yù)請求 (preflight request) 中」Access-Control-Allow-Headers: 請求所允許的頭,「用于預(yù)請求 (preflight request) 中」Access-Control-Max-Age: 預(yù)請求的緩存時(shí)間
寫一個(gè) CORS Middleware
既然 CORS 原理如此簡單,那就拿起鍵盤寫一個(gè)簡單的 CORS 中間件吧,CORS 大致是設(shè)置幾個(gè)響應(yīng)頭吧
?關(guān)于 cors 的響應(yīng)頭有哪些?[2]
?
「關(guān)于 CORS 的設(shè)置即是對 CORS 相關(guān)響應(yīng)頭的設(shè)置,因此了解這些 headers 至關(guān)重要。無論對于配置的生產(chǎn)者和消費(fèi)者,及后端和前端而言,都應(yīng)該掌握!」
以下是關(guān)于 CORS 相關(guān)的 response headers 及其釋義
Access-Control-Allow-Origin: 可以把資源共享給那些域名,支持 * 及 特定域名Access-Control-Allow-Credentials: 請求是否可以帶 cookieAccess-Control-Allow-Methods: 請求所允許的方法,?「用于預(yù)請求 (preflight request) 中」Access-Control-Allow-Headers: 請求所允許的頭,「用于預(yù)請求 (preflight request) 中」Access-Control-Expose-Headers: 那些頭可以在響應(yīng)中列出Access-Control-Max-Age: 預(yù)請求的緩存時(shí)間
而關(guān)于 CORS 的中間件即是使用默認(rèn)值與配置來設(shè)置這些頭,如?koa/cors?需要傳遞以下參數(shù)。
/**
?*?CORS?middleware
?*
?*?@param?{Object}?[options]
?*??-?{String|Function(ctx)}?origin?`Access-Control-Allow-Origin`,?default?is?request?Origin?header
?*??-?{String|Array}?allowMethods?`Access-Control-Allow-Methods`,?default?is?'GET,HEAD,PUT,POST,DELETE,PATCH'
?*??-?{String|Array}?exposeHeaders?`Access-Control-Expose-Headers`
?*??-?{String|Array}?allowHeaders?`Access-Control-Allow-Headers`
?*??-?{String|Number}?maxAge?`Access-Control-Max-Age`?in?seconds
?*??-?{Boolean|Function(ctx)}?credentials?`Access-Control-Allow-Credentials`,?default?is?false.
?*??-?{Boolean}?keepHeadersOnError?Add?set?headers?to?`err.header`?if?an?error?is?thrown
?*?@return?{Function}?cors?middleware
?*?@api?public
?*/
//?Example
app.use(cors())
CORS 如何設(shè)置多域名
由上,貌似很簡單,只需要服務(wù)端設(shè)置一下?Access-Control-Allow-Origin?就可以輕松解決問題,但其中的坑有可能比你想象地要多很多!
先說回?Access-Control-Allow-Origin,它所允許的值只有兩個(gè)
*: 所有域名shanyue.tech: 特定域名
此時(shí),新問題來了:
?CORS 如果需要指定多個(gè)域名怎么辦[3]
?
「如果使用?Access-Control-Allow-Origin: *,則所有的請求不能夠攜帶?cookie」,因此這種方案被擯棄。
因此這個(gè)問題需要寫代碼來解決,根據(jù)請求頭中的 Origin 來設(shè)置響應(yīng)頭?Access-Control-Allow-Origin
如果請求頭不帶有 Origin,證明未跨域,則不作任何處理 如果請求頭帶有 Origin,證明跨域,根據(jù) Origin 設(shè)置相應(yīng)的? Access-Control-Allow-Origin:
//?獲取?Origin?請求頭
const?requestOrigin?=?ctx.get('Origin');
//?如果沒有,則跳過
if?(!requestOrigin)?{
??return?await?next();
}
//?設(shè)置響應(yīng)頭
ctx.set('Access-Control-Allow-Origin',?requestOrigin)
「但此時(shí)會出現(xiàn)一個(gè)新的問題:緩存」
CORS 與 Vary: Origin
在討論與?Vary?關(guān)系時(shí),先拋出一個(gè)問題:
?如何避免 CDN 為 PC 端緩存移動端頁面[4]
?
假設(shè)有兩個(gè)域名訪問?static.shanyue.tech?的跨域資源
foo.shanyue.tech,響應(yīng)頭中返回?Access-Control-Allow-Origin: foo.shanyue.techbar.shanyue.tech,響應(yīng)頭中返回?Access-Control-Allow-Origin: bar.shanyue.tech
看起來一切正常,但平靜的水面下波濤暗涌:
「如果?static.shanyue.tech?資源被 CDN 緩存,bar.shanyue.tech?再次訪問資源時(shí),因緩存問題,因此此時(shí)返回的是?Access-Control-Allow-Origin: foo.shanyue.tech,此時(shí)會有跨域問題」
此時(shí),Vary: Origin?就上場了,代表為不同的?Origin?緩存不同的資源,這在各個(gè)服務(wù)器端 CORS 中間件也能體現(xiàn)出來,如以下幾段代碼
此處是一段 koa 關(guān)于 CORS 的處理函數(shù): 詳見?koajs/cors[5]
return?async?function?cors(ctx,?next)?{
??//?If?the?Origin?header?is?not?present?terminate?this?set?of?steps.
??//?The?request?is?outside?the?scope?of?this?specification.
??const?requestOrigin?=?ctx.get('Origin');
??//?Always?set?Vary?header
??//?https://github.com/rs/cors/issues/10
??ctx.vary('Origin');
}
此處是一段 Go 語言關(guān)于 CORS 的處理函數(shù): 詳見?rs/cors[6]
func?(c?*Cors)?handleActualRequest(w?http.ResponseWriter,?r?*http.Request)?{
?headers?:=?w.Header()
?origin?:=?r.Header.Get("Origin")
?//?Always?set?Vary,?see?https://github.com/rs/cors/issues/10
??headers.Add("Vary",?"Origin")
}
進(jìn)一步改進(jìn)相關(guān)代碼:
//?獲取?Origin?請求頭
const?requestOrigin?=?ctx.get('Origin');
//?不管有沒有跨域都要設(shè)置?Vary:?Origin
ctx.set('Vary',?'Origin')
//?如果沒有設(shè)置,說明沒有跨域,跳過
if?(!requestOrigin)?{
??return?await?next();
}
//?設(shè)置響應(yīng)頭
ctx.set('Access-Control-Allow-Origin',?requestOrigin)
「那此時(shí)是不關(guān)于?CORS?的問題就解決了?從中間件處理層面是這樣的,但仍然有一些服務(wù)端中間件使用問題及瀏覽器問題」
HSTS 與 CORS
HSTS (HTTP Strict Transport Security) 為了避免 HTTP 跳轉(zhuǎn)到 HTTPS 時(shí)遭受潛在的中間人攻擊,由瀏覽器本身控制到 HTTPS 的跳轉(zhuǎn)。如同 CORS 一樣,它也是有一個(gè)服務(wù)器的響應(yīng)頭來控制
Strict-Transport-Security: max-age=5184000
此時(shí)瀏覽器訪問該域名時(shí),會使用?307 Internal Redirect,無需服務(wù)器干涉,自動跳轉(zhuǎn)到 HTTPS 請求。
「如果前端訪問 HTTP 跨域請求,此時(shí)瀏覽器通過 HSTS 跳轉(zhuǎn)到 HTTPS,但瀏覽器不會給出相應(yīng)的 CORS 響應(yīng)頭部,就會發(fā)生跨域問題。」
GET / HTTP/1.1
Host: shanyue.tech
Origin: http://shanyue.tech
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
服務(wù)器異常處理與跨域異常
當(dāng)與其他中間件一起工作時(shí),也有可能出現(xiàn)問題,由于不正確的執(zhí)行順序也可能導(dǎo)致跨域失敗。
假設(shè)有一個(gè)參數(shù)校驗(yàn)中間件,置于 CORS 中間件上方,由于校驗(yàn)失敗,并未穿過 CORS 中間件,在前端會報(bào)錯(cuò)跨域失敗,真正的參數(shù)校驗(yàn)問題掩蓋其中。
const?Koa?=?require('koa')
const?app?=?new?Koa()
const?cors?=?require('@koa/cors')
//?異常處理中間件
app.use(async?(ctx,?next)?=>?{
??try?{
????await?next()
??}?catch?(e)?{
????ctx.body?=?'hello,?error'
??}
})
//?某一個(gè)特定時(shí)刻肯定會報(bào)錯(cuò)的中間件
app.use(async?(ctx,?next)?=>?{
??throw?new?Error('hello,?world')
})
//?CORS?中間件
app.use(cors())
app.listen(3000)
總結(jié)
本篇文章介紹了跨域問題及其相應(yīng)的 CORS 解決方案,并列出了若干細(xì)節(jié)問題。
CORS 通過服務(wù)器端設(shè)置若干響應(yīng)頭來正常工作 Access-Control-Allow-Origin: *?無法攜帶 Cookie,因此以此為多域名跨域設(shè)置有缺陷服務(wù)器端通過響應(yīng)頭? Origin?來判斷是否為跨域請求,并以此設(shè)置多域名跨域,但要加上?Vary: Origin在編碼過程中要注意 HSTS 配置及服務(wù)器的中間件順序帶來的潛在風(fēng)險(xiǎn)
Reference
什么是跨域?:https://q.shanyue.tech/fe/js/216.html
[2]關(guān)于 cors 的響應(yīng)頭有哪些?:https://q.shanyue.tech/base/http/328.html
[3]CORS 如果需要指定多個(gè)域名怎么辦:https://q.shanyue.tech/base/http/364.html
[4]如何避免 CDN 為 PC 端緩存移動端頁面:https://q.shanyue.tech/base/http/330.html
[5]koajs/cors:https://github.com/koajs/cors/blob/master/index.js#L54
[6]rs/cors:https://github.com/rs/cors/blob/be1c7e127af9fce006600894df5c5731d99cdc82/cors.go#L268
掃碼關(guān)注公眾號,訂閱更多精彩內(nèi)容。
給個(gè)[在看],是對達(dá)達(dá)最大的支持!

