CORS(cross-origin resource sharing)厌衙,跨源資源共享(一般俗稱(chēng)『跨域請(qǐng)求』),想必大家都已經(jīng)有基本的了解。如果你還不了解的話构挤,可以閱讀MDN 上的介紹 (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) ,這里就不贅述了惕鼓。
不過(guò)在學(xué)習(xí)CORS時(shí)筋现,有些朋友會(huì)有疑惑,為什么CORS要把請(qǐng)求分成兩類(lèi):簡(jiǎn)單請(qǐng)求和預(yù)檢請(qǐng)求(preflighted requests)呢箱歧?
如果我們看簡(jiǎn)單請(qǐng)求和預(yù)檢請(qǐng)求的區(qū)分矾飞,會(huì)看到有很多的條件:
簡(jiǎn)單請(qǐng)求的 HTTP 方法只能是 GET、HEAD 或 POST
簡(jiǎn)單請(qǐng)求的 HTTP 頭只能是 Accept/Accept-Language/Conent-Language/Content-Type 等
簡(jiǎn)單請(qǐng)求的 Content-Type 頭只能是 text/plain呀邢、multipart/form-data 或 application/x-www-form-urlencoded
看上去很是復(fù)雜洒沦。
那么怎么理解這些限制呢?
其實(shí)驼鹅,簡(jiǎn)單請(qǐng)求就是普通 HTML Form 在不依賴(lài)腳本的情況下可以發(fā)出的請(qǐng)求微谓,比如表單的 method 如果指定為 POST 森篷,可以用 enctype 屬性指定用什么方式對(duì)表單內(nèi)容進(jìn)行編碼,合法的值就是前述這三種豺型。
非簡(jiǎn)單請(qǐng)求就是普通 HTML Form 無(wú)法實(shí)現(xiàn)的請(qǐng)求仲智。比如 PUT 方法、需要其他的內(nèi)容編碼方式姻氨、自定義頭之類(lèi)的钓辆。
對(duì)于服務(wù)器來(lái)說(shuō),第一肴焊,許多服務(wù)器壓根沒(méi)打算給跨源用前联。當(dāng)然你不給 CORS 響應(yīng)頭,瀏覽器也不會(huì)使用響應(yīng)結(jié)果娶眷,但是請(qǐng)求本身可能已經(jīng)造成了后果似嗤。所以最好是默認(rèn)禁止跨源請(qǐng)求。
第二届宠,要回答某個(gè)請(qǐng)求是否接受跨源烁落,可能涉及額外的計(jì)算邏輯。這個(gè)邏輯可能很簡(jiǎn)單豌注,比如一律放行伤塌。也可能比較復(fù)雜,結(jié)果可能取決于哪個(gè)資源哪種操作來(lái)自哪個(gè) origin轧铁。對(duì)瀏覽器來(lái)說(shuō)每聪,就是某個(gè)資源是否允許跨源這么簡(jiǎn)單;對(duì)服務(wù)器來(lái)說(shuō)齿风,計(jì)算成本卻可大可小药薯。所以我們希望最好不用每次請(qǐng)求都讓服務(wù)器勞神計(jì)算。
CORS-preflight 就是這樣一種機(jī)制救斑,瀏覽器先單獨(dú)請(qǐng)求一次果善,詢(xún)問(wèn)服務(wù)器某個(gè)資源是否可以跨源,如果不允許的話就不發(fā)實(shí)際的請(qǐng)求系谐。注意先許可再請(qǐng)求等于默認(rèn)禁止了跨源請(qǐng)求巾陕。如果允許的話,瀏覽器會(huì)記住纪他,然后發(fā)實(shí)際請(qǐng)求鄙煤,且之后每次就都直接請(qǐng)求而不用再詢(xún)問(wèn)服務(wù)器否可以跨源了。于是茶袒,服務(wù)器想支持跨源梯刚,就只要針對(duì) preflight 進(jìn)行跨源許可計(jì)算。本身真正的響應(yīng)代碼則完全不管這個(gè)事情薪寓。并且因?yàn)?preflight 是許可式的亡资,也就是說(shuō)如果服務(wù)器不打算接受跨源澜共,什么事情都不用做。
但是這機(jī)制只能限于非簡(jiǎn)單請(qǐng)求锥腻。在處理簡(jiǎn)單請(qǐng)求的時(shí)候嗦董,如果服務(wù)器不打算接受跨源請(qǐng)求,不能依賴(lài) CORS-preflight 機(jī)制瘦黑。因?yàn)椴煌ㄟ^(guò) CORS京革,普通表單也能發(fā)起簡(jiǎn)單請(qǐng)求,所以默認(rèn)禁止跨源是做不到的幸斥。
既然如此匹摇,簡(jiǎn)單請(qǐng)求發(fā) preflight 就沒(méi)有意義了,就算發(fā)了服務(wù)器也省不了后續(xù)每次的計(jì)算甲葬,反而在一開(kāi)始多了一次 preflight廊勃。
有些人把簡(jiǎn)單請(qǐng)求不需要 preflight 理解為『向下兼容』。這也不能說(shuō)錯(cuò)经窖。但嚴(yán)格來(lái)說(shuō)供搀,并不是『為了向下兼容』而不能發(fā)。理論上瀏覽器可以區(qū)別對(duì)待表單請(qǐng)求和非表單請(qǐng)求 —— 對(duì)傳統(tǒng)的跨源表單提交不發(fā) preflight钠至,從而保持兼容,只對(duì)非表單跨源請(qǐng)求發(fā) preflight胎源。
但這樣做并沒(méi)有什么好處棉钧,反而把事情搞復(fù)雜了。比如本來(lái)你可以直接用腳本發(fā)跨源普通請(qǐng)求涕蚤,盡管(在服務(wù)器默認(rèn)沒(méi)有跨源處理的情況下)你無(wú)法得到響應(yīng)結(jié)果宪卿,但是你的需求可能只是發(fā)送無(wú)需返回,比如打個(gè)日志万栅。但現(xiàn)在如果服務(wù)器不理解 preflight 你就干不了這個(gè)事情了佑钾。
而且如果真的這樣做,服務(wù)器就變成了默認(rèn)允許跨源表單烦粒,如果想控制跨源休溶,還是得(跟原本一樣)直接在響應(yīng)處理中執(zhí)行跨源計(jì)算邏輯;另一方面服務(wù)器又需要增加對(duì) preflight 請(qǐng)求的響應(yīng)支持扰她,執(zhí)行類(lèi)似的跨源計(jì)算邏輯以控制來(lái)自非表單的相同跨源請(qǐng)求兽掰。服務(wù)器通常沒(méi)有區(qū)分表單/非表單差異的需求,這樣搞純粹是折騰服務(wù)器端工程師徒役。
所以簡(jiǎn)單請(qǐng)求不發(fā) preflight 不是因?yàn)椴荒芗嫒菽蹙。且驗(yàn)榧嫒莸那疤嵯掳l(fā) preflight 對(duì)絕大多數(shù)服務(wù)器應(yīng)用來(lái)說(shuō)沒(méi)有意義,反而把問(wèn)題搞復(fù)雜了忧勿。
- 補(bǔ)充1
賀師俊 (作者) :
絕對(duì)意義上的后端安全是另一個(gè)層面的事情杉女,不要混在一起理解瞻讽。簡(jiǎn)單請(qǐng)求不存在『繞過(guò)』的問(wèn)題。從有網(wǎng)站開(kāi)始熏挎,簡(jiǎn)單請(qǐng)求就一直可以跨源提交速勇。所以服務(wù)器如果要禁止簡(jiǎn)單請(qǐng)求的跨源提交,從來(lái)就是要自己處理的婆瓜。而節(jié)省跨源計(jì)算只能針對(duì)新的需求快集,也就是原本瀏覽器不可能發(fā)送的跨源非簡(jiǎn)單請(qǐng)求。你確實(shí)可以把簡(jiǎn)單請(qǐng)求不需要preflight理解為『為了向下兼容』廉白。但嚴(yán)格來(lái)說(shuō)个初,不是。理論上瀏覽器可以對(duì)傳統(tǒng)的跨源表單提交不發(fā)preflight猴蹂,從而保持兼容院溺,只對(duì)腳本發(fā)起的跨源表單提交發(fā)preflight。這樣服務(wù)器這里默認(rèn)允許跨源表單提交磅轻,但通過(guò)響應(yīng)preflight來(lái)控制腳本的相同跨源請(qǐng)求珍逸。但是服務(wù)器通常沒(méi)有區(qū)分這種微秒差異的需求。所以不發(fā)preflight不是因?yàn)椴荒芗嫒萘铮且驗(yàn)榧嫒莸那疤嵯掳l(fā)preflight對(duì)絕大多數(shù)服務(wù)器應(yīng)用來(lái)說(shuō)沒(méi)有意義谆膳。
*補(bǔ)充2
Ivony:
如果我們現(xiàn)在重新設(shè)計(jì)整個(gè)HTTP協(xié)議,我們可以要求瀏覽器在發(fā)送任何數(shù)據(jù)到另外一個(gè)域的服務(wù)器之前撮躁,都必須先發(fā)送preflight request漱病。但是大部分現(xiàn)存網(wǎng)站并未針對(duì)preflight request做出實(shí)現(xiàn),所以這意味著現(xiàn)有的互聯(lián)網(wǎng)中把曼,如果一個(gè)域的表單向另一個(gè)域提交的時(shí)候會(huì)跨域失敗杨帽,直到目標(biāo)網(wǎng)站更新處理perflight request為止。所以在我們制定這一新的標(biāo)準(zhǔn)的時(shí)候嗤军,應(yīng)當(dāng)考慮到目前互聯(lián)網(wǎng)已經(jīng)存在這樣的請(qǐng)求注盈,他們雖然看起來(lái)可能不安全,但為了向下兼容叙赚,我們不能強(qiáng)制對(duì)這些請(qǐng)求做preflight request老客。既然不能強(qiáng)制做preflight request驗(yàn)證,那發(fā)這個(gè)東西就沒(méi)有什么意義了震叮。當(dāng)然沿量,我認(rèn)為在時(shí)機(jī)成熟的時(shí)候,我們可以引入一種強(qiáng)制CORS的機(jī)制冤荆,就像現(xiàn)在的強(qiáng)制HTTPS機(jī)制一樣朴则。我們可以約定瀏覽器預(yù)先發(fā)一個(gè)請(qǐng)求到目標(biāo)域名確定目標(biāo)域的服務(wù)器是否支持強(qiáng)制CORS。如果目標(biāo)域支持強(qiáng)制CORS,則瀏覽器對(duì)引用目標(biāo)域的任何資源請(qǐng)求都發(fā)出Origin頭乌妒,任何數(shù)據(jù)的發(fā)送都先發(fā)送preflight request陨簇。至于你的迷惑赋除,簡(jiǎn)單來(lái)說(shuō):CORS是允許受限的跨域訪問(wèn)侈玄,不是限制現(xiàn)有的跨域訪問(wèn)宜肉。沒(méi)有CORS之前我們不是不可以跨域訪問(wèn),而是要很彎彎繞(譬如說(shuō)JSONP和萬(wàn)能的服務(wù)器代發(fā))侦啸,而CORS則是提出一個(gè)方案可以讓我們直接了當(dāng)?shù)拿枋隹缬蛟L問(wèn)的需求并且加以控制槽唾。
*補(bǔ)充3
失禮:
簡(jiǎn)單請(qǐng)求的情況不需求發(fā)起preflight,也就不用先發(fā)起options請(qǐng)求然后再發(fā)起真實(shí)的請(qǐng)求光涂,也就是說(shuō)如果剛好允許跨域或者壓根沒(méi)跨域庞萍,它消耗資源少。簡(jiǎn)單請(qǐng)求的那些情況就是我們常用的操作忘闻,如果也需要preflight钝计,就浪費(fèi)資源了。因?yàn)閷?duì)我們自己而言的大量非跨域請(qǐng)求就不公平了齐佳。而對(duì)于非簡(jiǎn)單請(qǐng)求而言私恬,瀏覽器實(shí)行跨域預(yù)檢機(jī)制可以節(jié)約資源,也做了一道防線炼吴。因?yàn)槿绻蠖瞬辉试S跨域本鸣,就不需要發(fā)送正式的請(qǐng)求啦。退一步講硅蹦,正常后端都是會(huì)對(duì)跨域請(qǐng)求做過(guò)濾限制的荣德。不管你是簡(jiǎn)單還是復(fù)雜請(qǐng)求。