同源策略,解決跨域問(wèn)題方法及原理

前言

Cross-Origin Resource Sharing(CORS) 是W3C為瀏覽器制定的可以跨域通信的規(guī)范. 通過(guò)使用 XMLHttpRequest 對(duì)象, CORS可以讓開發(fā)者方便的進(jìn)行跨域通信, 就像在使用同域通信一樣.

CORS的使用十分簡(jiǎn)單. 想象一下有一個(gè)網(wǎng)站 a.com 想要獲取另一個(gè)網(wǎng)站 b.com 的數(shù)據(jù). 但由于瀏覽器的同源策略, 這樣的請(qǐng)求將會(huì)被禁止. 這時(shí)我們可以使用CORS, 通過(guò)添加一些特殊的請(qǐng)求\響應(yīng)頭, 可以讓 a.com 訪問(wèn) b.com 的數(shù)據(jù).

通過(guò)上面的例子我們可以看出, CORS的支持需要客戶端和服務(wù)器同時(shí)支持才行. 幸運(yùn)的是, 如果你是一名客戶端的開發(fā)人員(如前端工程師), 絕大多數(shù)的技術(shù)細(xì)節(jié)都會(huì)被隱藏掉.

這篇文章將講述客戶端如何發(fā)送一個(gè)跨域請(qǐng)求, 而服務(wù)器又將如何去處理和支持跨域請(qǐng)求.

發(fā)送一個(gè)跨域請(qǐng)求

時(shí)至今日, 發(fā)送一個(gè)XMLHttprequest請(qǐng)求已經(jīng)是一個(gè)簡(jiǎn)單的事情, 這里我不在過(guò)多贅述.

根據(jù)瀏覽器的同源策略, 當(dāng)請(qǐng)求的地址與來(lái)源地址的協(xié)議\域名\端口中的任一值不相同時(shí), 均視為是一個(gè)跨域的請(qǐng)求.

XMLHttprequest 的 withCredentials 屬性

跨域請(qǐng)求通常不會(huì)攜帶cookies信息. 為了能讓跨域請(qǐng)求帶上cookies, 你需要將做如下設(shè)置:

xhr.withCredentials = true;

為了能讓這個(gè)屬性正常工作, 你還需要在服務(wù)器端在響應(yīng)是帶上Access-Control-Allow-Credentials , 同時(shí)它的值必須為true. 更多的內(nèi)容可以看服務(wù)器設(shè)置的那一部分.

Access-Control-Allow-Credentials: true

設(shè)置withCredentials為true后, 在于服務(wù)器進(jìn)行通信時(shí)會(huì)攜帶這個(gè)域名下的所有cookies, 同時(shí)服務(wù)器也可以在它的于域名下設(shè)置cookies. 但值得注意的是, 這些cookies仍然遵守瀏覽器的同源策略, 你無(wú)法通過(guò)javascript訪問(wèn)這個(gè)域名下的cookies, 它只被這個(gè)域名的服務(wù)器控制.

為服務(wù)器增加跨域請(qǐng)求的支持

大部分跨域請(qǐng)求的重要操作實(shí)在瀏覽器和服務(wù)器之間進(jìn)行的. 瀏覽器在跨域請(qǐng)求期間會(huì)代表客戶端在請(qǐng)求上增加行的請(qǐng)求頭, 有的時(shí)候還會(huì)增加新的請(qǐng)求. 這些操作對(duì)于開發(fā)者來(lái)說(shuō)是透明, 但是這些請(qǐng)求仍然可以被抓包工具捕獲.

跨域請(qǐng)求

瀏覽器的開發(fā)者來(lái)實(shí)現(xiàn)瀏覽器端的跨域請(qǐng)求細(xì)節(jié), 這節(jié)內(nèi)容來(lái)介紹如何配置服務(wù)器來(lái)支持跨域請(qǐng)求.

跨域請(qǐng)求的分類

通常將跨越請(qǐng)求分為"簡(jiǎn)單請(qǐng)求"和"非簡(jiǎn)單請(qǐng)求"兩類.

簡(jiǎn)單請(qǐng)求遵循以下的規(guī)則

簡(jiǎn)單請(qǐng)求

首先它的請(qǐng)求方式只能是GET, POST, PUT

同時(shí)的它的請(qǐng)求頭部只能包含上面的那幾個(gè)類型, 值的注意的是Content-Type只能是羅列出的三種(正好是form的entryType的三個(gè)值).

對(duì)于簡(jiǎn)單請(qǐng)求, 瀏覽器可以自行解決其中的跨域問(wèn)題. 例如我們熟知的一個(gè)跨域通信的解決方式JSON-P就是利用GET發(fā)送一個(gè)簡(jiǎn)單請(qǐng)求來(lái)規(guī)避跨域的問(wèn)題.HTML中的表單提交也不需要處理跨域問(wèn)題.

任何不符合上述條件的請(qǐng)求都算作非簡(jiǎn)單請(qǐng)求, 瀏覽器在處理非簡(jiǎn)單的跨域請(qǐng)求時(shí)會(huì)與服務(wù)器進(jìn)行額外的通信(稱之為預(yù)檢請(qǐng)求), 將在下面介紹.

處理簡(jiǎn)單請(qǐng)求

通過(guò)這個(gè)cors-demo我們可以方便的查看瀏覽器與服務(wù)器之間的同信.

當(dāng)瀏覽器發(fā)送一個(gè)簡(jiǎn)單請(qǐng)求時(shí), 我們打開瀏覽器的Network面板可以看到一個(gè)如下的請(qǐng)求(刪除部分內(nèi)容)

GET /get HTTP/1.1
Host: localhost:5051
Origin: http://localhost:8080
User-Agent: Mozilla/5.0 ....
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,en;q=0.8,zh;q=0.6,ja;q=0.4,zh-TW;q=0.2

我們需要注意的一點(diǎn)是, 所有的跨域請(qǐng)求(無(wú)論簡(jiǎn)單或者非簡(jiǎn)單)總會(huì)包含一個(gè)Origin的請(qǐng)求頭部, 這個(gè)屬性的值由瀏覽器添加, 而且不受用戶控制. 它的值由協(xié)議(如: http), 域名(如: a.om)和端口(只有它不是默認(rèn)值時(shí)才包含, 如80端口)組成, 說(shuō)明請(qǐng)求的來(lái)源.

包含Origin的請(qǐng)求不一定是跨域請(qǐng)求, 但是跨域請(qǐng)求一定包含Origin. 一些同源的請(qǐng)求同樣也會(huì)包含Origin請(qǐng)求頭.例如, Firefox瀏覽器不會(huì)在同源的請(qǐng)求中添加Origin, 但是Chrome和Safari會(huì)在同源的POST/PUT/DELETE請(qǐng)求中添加Origin請(qǐng)求頭(但是同源的GET不會(huì)添加).

瀏覽器會(huì)忽略掉同源請(qǐng)求中的的CORS響應(yīng)中的設(shè)置.

然后我們來(lái)看一個(gè)有效的跨域請(qǐng)求響應(yīng)

Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

所有與跨域請(qǐng)求相關(guān)的HTTP頭部都以Access-Control-開始, 下面是它們的詳細(xì)信息

  1. Access-Control-Allow-Origin (必選)

    所有有效的跨域響應(yīng)都必須包含這個(gè)請(qǐng)求頭, 沒(méi)有的話會(huì)導(dǎo)致跨域請(qǐng)求失敗. 它的值可以是請(qǐng)求中的Origin的值, 也可以設(shè)置為*來(lái)表示可以響應(yīng)所有來(lái)源的請(qǐng)求.

  2. Access-Control-Allow-Credentials (可選)

    默認(rèn)情況下跨域請(qǐng)求不會(huì)攜帶cookies信息. 如果需要請(qǐng)求攜帶cookies信息, 則需要將這個(gè)值設(shè)置為true, 如果不需要就不要設(shè)置這個(gè)值, 而不是將它設(shè)置為false.

    這個(gè)請(qǐng)求頭需要與 [withCredentials](#XMLHttprequest 的 withCredentials 屬性) 配合使用. 只有兩個(gè)值都設(shè)置為true的時(shí)候才能夠在請(qǐng)求中攜帶cookies信息. 當(dāng)withCredentials設(shè)置為true, 而響應(yīng)中不包含Access-Control-Allow-Credentials時(shí), 請(qǐng)求會(huì)發(fā)生錯(cuò)誤.

  3. Access-Control-Expose-Headers (可選)

    XMLHttpRequest2對(duì)象上的getResponseHeader()方法可以讓你獲取到響應(yīng)中頭部信息, 但在跨域請(qǐng)求中,你只能獲取到以下信息

    • Cache-Control
    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma

    如果你希望客戶端能過(guò)獲取其他的頭部信息, 可以設(shè)置這個(gè)值.

處理一個(gè)非簡(jiǎn)單請(qǐng)求

對(duì)與開發(fā)者來(lái)說(shuō), 發(fā)送一個(gè)跨域的非簡(jiǎn)單請(qǐng)求跟發(fā)送一個(gè)同域請(qǐng)求沒(méi)什么區(qū)別.但事實(shí)上瀏覽器會(huì)發(fā)送兩個(gè)請(qǐng)求, 第一個(gè)請(qǐng)求(成為預(yù)檢請(qǐng)求)會(huì)像服務(wù)器確定是否接受這個(gè)跨域請(qǐng)求, 第二個(gè)才是真正的發(fā)出請(qǐng)求. 瀏覽器自動(dòng)的處理這兩個(gè)請(qǐng)求, 同時(shí)預(yù)檢請(qǐng)求也是可以被緩存的, 而不用每次請(qǐng)求都需要發(fā)送預(yù)檢請(qǐng)求.

下面是一個(gè)預(yù)檢請(qǐng)求

OPTIONS /cors/post HTTP/1.1
Host: localhost:5051
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Origin: http://localhost:8080
User-Agent: Mozilla/5.0 ...
DNT: 1
Referer: http://localhost:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,en;q=0.8,zh;q=0.6,ja;q=0.4,zh-TW;q=0.2

如同簡(jiǎn)單跨域請(qǐng)求一樣, 在預(yù)檢請(qǐng)求中也包含了Origin請(qǐng)求頭, 同時(shí)這個(gè)請(qǐng)求的方式OPTIONS(所以你必須確定你的服務(wù)器能夠正常的處理這中請(qǐng)求). 它同時(shí)也包含了其他的請(qǐng)求頭.

  1. Access-Control-Request-Method

    這個(gè)請(qǐng)求頭的值就是正式請(qǐng)求的請(qǐng)求方式, 上面的那個(gè)例子就是POST

  2. Access-Control-Request-Headers

    它的值是一個(gè)由逗號(hào)分隔的正式請(qǐng)求中請(qǐng)求頭的列表.

預(yù)檢請(qǐng)求是在實(shí)際的請(qǐng)求發(fā)出前先向服務(wù)器確認(rèn)是否能夠處理這個(gè)請(qǐng)求. 服務(wù)器應(yīng)該檢查上邊兩個(gè)請(qǐng)求頭的值, 來(lái)判斷這個(gè)請(qǐng)求是否有效.

如果服務(wù)器確認(rèn)這個(gè)請(qǐng)求有效, 那么它會(huì)做出如下的響應(yīng)

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Content-Type: text/html
Date: Sat, 12 Aug 2017 13:46:08 GMT
Connection: keep-alive
Transfer-Encoding: chunked
  1. Access-Control-Allow-Origin (必選)

    如同簡(jiǎn)單跨域請(qǐng)求一樣, 預(yù)檢請(qǐng)求的響應(yīng)也必須包含這個(gè)值

  2. Access-Control-Allow-Methods (必選)

    由逗號(hào)分隔的HTTP請(qǐng)求方式, 在其中的值表示服務(wù)能夠接受這中請(qǐng)求方式的跨域請(qǐng)求.

    值得注意的是雖然預(yù)檢請(qǐng)求只是針對(duì)單個(gè)請(qǐng)求方式進(jìn)行檢測(cè), 但是你仍可以返回一個(gè)你所支持的請(qǐng)求方式列表.這樣做的好處是方便對(duì)預(yù)檢請(qǐng)求進(jìn)行緩存.

  3. Access-Control-Allow-Headers (當(dāng)預(yù)檢請(qǐng)求中包含Access-Control-Request-Headers時(shí)是必須的)

    由逗號(hào)分隔的支持的請(qǐng)求頭部列表, 與Access-Control-Allow-Methods類似, 雖然預(yù)檢請(qǐng)求中只有很少的一部分請(qǐng)求頭, 但是你仍然可以返回所有你支持的列表, 原因也是為了緩存.

  4. Access-Control-Allow-Credentials (可選)

    同簡(jiǎn)單請(qǐng)求

  5. Access-Control-Max-Age (可選)

    在每個(gè)請(qǐng)求前面都發(fā)送一個(gè)預(yù)檢請(qǐng)求是很浪費(fèi)資源的, 這個(gè)值允許你設(shè)置預(yù)檢請(qǐng)求的緩存時(shí)間, 單位是秒.

一旦預(yù)檢請(qǐng)求通過(guò)服務(wù)器的檢查, 那么瀏覽器會(huì)隨后發(fā)送實(shí)際的請(qǐng)求. 實(shí)際請(qǐng)求的處理與簡(jiǎn)單請(qǐng)求一樣.

如果服務(wù)器想要拒絕一個(gè)跨域請(qǐng)求, 那么他可以直接回復(fù)一個(gè)簡(jiǎn)單的響應(yīng)(如 HTTP 200), 但在響應(yīng)頭中不要包含任何與CORS相關(guān)的響應(yīng)頭設(shè)置. 服務(wù)器也可能會(huì)因?yàn)轭A(yù)檢請(qǐng)求不合法而拒絕這個(gè)請(qǐng)求. 如果預(yù)檢請(qǐng)求中不包含正確的CORS頭部設(shè)置, 它就不會(huì)發(fā)送實(shí)際的請(qǐng)求.

當(dāng)跨域請(qǐng)求發(fā)生錯(cuò)時(shí), 瀏覽器會(huì)調(diào)用onerror事件, 同時(shí)會(huì)在控制臺(tái)打印相關(guān)的錯(cuò)誤信息.

cors

最后附上一個(gè)服務(wù)器端處理跨域請(qǐng)求的流程圖

服務(wù)器處理跨域請(qǐng)求流程圖

轉(zhuǎn)載至慕課網(wǎng)
鏈接:https://www.imooc.com/article/19869?block_id=tuijian_wz

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容