CORS(跨域资源共享)
CORS(跨域資源共享)使用額外的HTTP頭部來告訴瀏覽器,允許運行在origin(domain)上的Web應用訪問來自不同源服務器上的指定資源。
瀏覽器訪問一個web應用,這個web應用會發很多的跨域請求,例如加載不同源的JS/CSS腳本,或者加載不同源圖片等。但是并沒有發現請求的異常,這些資源是可以正常返回的。而通過JS發送的跨域HTTP請求卻時常得到錯誤,所以跨域請求很常見,但是瀏覽器對于請求跨域的限制卻只存在于腳本發送的HTTP請求(Ajax/Fetch)。
同源限制在安全上是有其必要性的,例如可以很輕松規避CSRF攻擊。
通過上面的描述可以看出來同源策略的限制存在于瀏覽器端,而CORS策略用額外的HTTP請求頭字段來告訴瀏覽器,該資源是允許被當前域上的web應用跨域訪問的。
CORS的具體步驟
那么具體CORS是怎么做的呢?
上面說到“通過額外的HTTP頭告訴瀏覽器源上的web應用被允許訪問來自不同源服務器上的指定資源”。告訴瀏覽器的自然是通過服務端的響應攜帶的額外的HTTP頭,所以可以看出來請求是發送了的。那么服務端是攜帶了標識當前源允許訪問的該資源的HTTP頭?如果允許的話自然是請求一切正常,不允許的話瀏覽器則會報錯并且不會將請求結果返回給請求的發起方,也就是Ajax/Fetch代碼,并且代碼中獲取不到是哪一步出了錯只能在瀏覽器中看到錯誤日志(為了安全)。
那么就只是這樣嗎?跨域請求失敗是因為瀏覽器正常發送了請求,服務端正常響應了請求,然后瀏覽器發覺響應頭中沒有允許跨域的標識,然后攔截返回結果并報錯。
并不完全是這樣,這只是CORS的一部分,這部分被稱為簡單請求。
既然有簡單請求就有非簡單請求。非簡單請求的具體步驟和上面描述的簡單請求很不一樣,會在發送真正的請求之前發送一個預檢請求(preflight request)詢問服務端是否允許當前源跨域訪問該資源,允許則繼續發送真正的請求,否則直接報錯。
所以上面的兩種做法都被應用到CORS策略中:一種是攔截請求的返回結果,一種是不發送真正的跨域請求。
簡單請求
簡單請求并不會發送預檢請求,而是直接發送真正的請求。簡單請求必須要全部滿足下面的條件:
- 使用下列方法之一:
- GET
- HEAD
- POST
- 不得人為設置該集合之外的其他首部字段。該集合為:
- Accep
- Accept-Language
- Content-Language
- Content-Type(需要注意額外的限制)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- Content-Type 的值僅限于下列三者之一:(這個集合中沒有application/json)
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- 請求中的任意XMLHttpRequestUpload`對象均沒有注冊任何事件監聽器;XMLHttpRequestUpload對象可以使用 XMLHttpRequest.upload 屬性訪問。
- 請求中沒有使用ReadableStream對象。
下面我們將從源http://dev.jd.com:9091訪問http://dev.jd.com:9090的資源。
如果沒有使用CORS顯然會被瀏覽器的同源策略限制從而報錯,如下:
分別查看請求頭:
GET /corsget HTTP/1.1 Host: dev.jd.com:9090 Connection: keep-alive Pragma: no-cache Cache-Control: no-cache User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Accept: */* Origin: http://dev.jd.com:9091 Referer: http://dev.jd.com:9091/index.html Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8通過請求頭可以看出來其中包含了很多簡單請求定義的字段以外的字段,但是卻并沒有觸發預檢請求,這是因為這些頭字段并不是人為設置的。
響應頭:
HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 16 ETag: W/"10-oV4hJxRVSENxc/wX8+mA4/Pe4tA" Date: Mon, 06 Apr 2020 09:45:36 GMT Connection: keep-alive修改服務端程序,讓響應頭帶上標識,告訴瀏覽器允許源http://dev.jd.com:9091上的web應用訪問不同源(http://dev.jd.com:9090)上的資源/corsget。
請求頭同上,響應頭如下:
HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Content-Type: application/json; charset=utf-8 Content-Length: 16 ETag: W/"10-oV4hJxRVSENxc/wX8+mA4/Pe4tA" Date: Mon, 06 Apr 2020 09:58:06 GMT Connection: keep-alive可以看到多了一個Access-Control-Allow-Origin這個字段,該字段表示被允許訪問該資源的不同源,這里指定了*表示告訴瀏覽器任何源都可以訪問該資源。
通過觀察還可以發現請求頭中有字段Origin正好對應的是就是http://dev.jd.com:9091web應用所在的源。
非簡單請求
上面簡單請求的條件只要有一個沒有滿足就會變成非簡單請求。非簡單請求會首先發送一個預檢請求詢問服務端是否允許跨域,服務端允許后才會發送真正的請求。
注:chrome的network面板中看不到預檢請求,可以查看 chrome://flags/#out-of-blink-cors 配置,改成 disabled 后重啟 Chrome ,或者換個瀏覽器Firefox是可以看到的,目前是74.0版本。
預檢的請求頭:
OPTIONS /corsget HTTP/1.1 Host: dev.jd.com:9090 Connection: keep-alive Pragma: no-cache Cache-Control: no-cache Access-Control-Request-Method: GET Access-Control-Request-Headers: content-type Origin: http://dev.jd.com:9091 User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Accept: */* Referer: http://dev.jd.com:9091/index.html Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8通過人為修改請求頭的content-type為application/json將原本的簡單請求變成了非簡單請求,因為application/json并不在簡單請求的三種content-type中。
這樣就需要首先發送一個預檢請求。
可以看到請求頭中有如下字段:
Access-Control-Request-Method: GET
用于預檢請求,將實際發送的請求的method告知服務器
Access-Control-Request-Headers: content-type
用于預檢請求,將實際發送的請求頭(不滿足簡單請求條件)告知給服務器
Origin: http://dev.jd.com:9091
告訴服務器請求源
預檢響應頭:
HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Access-Control-Allow-Methods: OPTIONS Access-Control-Allow-Headers: Content-Type Content-Type: text/html; charset=utf-8 Content-Length: 0 ETag: W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk" Date: Mon, 06 Apr 2020 10:57:40 GMT Connection: keep-alive可以看到如下字段:
Access-Control-Allow-Origin: *
告訴瀏覽器任何源都可訪問該資源
Access-Control-Allow-Methods: OPTIONS
告訴瀏覽器options被允許跨域訪問該資源。options并不在簡單請求被允許的method中,但是預檢請求確是options,所以需要指定options被允許。
Access-Control-Allow-Headers: Content-Type
告訴瀏覽器三個Content-Type值之外的Content-Type值被允許跨域訪問該資源。因為application/json并不在簡單請求的三個content-type中。
然后發送實際的get請求,請求頭如下:
GET /corsget HTTP/1.1 Host: dev.jd.com:9090 Connection: keep-alive Pragma: no-cache Cache-Control: no-cache Origin: http://dev.jd.com:9091 User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Content-Type: application/json Accept: */* Referer: http://dev.jd.com:9091/index.html Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8實際請求的響應頭如下:
HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Content-Type: application/json; charset=utf-8 Content-Length: 16 ETag: W/"10-oV4hJxRVSENxc/wX8+mA4/Pe4tA" Date: Mon, 06 Apr 2020 10:57:40 GMT Connection: keep-alive可以看到實際請求的響應頭中并沒有復雜的訪問控制類型的HTTP頭,只有一個Access-Control-Allow-Origin: *。在實際請求的響應頭中這個字段是必須的,如果沒有這個頭部即使預檢通過了,實際發送的請求還是會失敗,返回的結果還是會被瀏覽器攔截,并不會返回給腳本,并報錯。
跨域請求和憑證
Ajax和Fetch的跨域請求默認不會攜帶憑證。可以通過Ajax/Fetch的設置讓發送請求的時候帶上憑證,但是如果服務端并不允許攜帶憑證的跨域請求,那么理所當然的跨域請求會失敗。
Ajax: xhr.withCredentials = true
Fetch: fetch(url, {credentials: 'include', mode: 'cors'})
當服務端設置 Access-Control-Allow-Credentials: true 允許跨域請求攜帶憑證的時候對于Access-Control-Allow-Origin還有一個限制,就是值不能為*。
那么我們就需要在后端設置上指定允許攜帶憑證跨域的源,如果有多個怎么辦呢?因為Access-Control-Allow-Origin這個字段并不能設置多個值,可以通過代碼獲取請求頭的Origin來判斷是否允許獲取該資源,允許的話將Origin值設置給響應的HTTP頭字段Access-Control-Allow-Origin即可。
仔細觀察就可以發現Access-Control-Allow-Credentials: true這個響應頭在預檢和實際請求的響應頭中被返回了兩次(一次都不能少,否則會報錯,一樣是跨域錯誤),但是之前設置的Access-Control-Allow-Headers: Content-Type只有在預檢的時候才會被返回。
其實關于Access-Control的HTTP頭還有一個是控制預檢請求的緩存時間的,就是Access-Control-Max-Age單位是秒。
所以初步猜測Access-Control-Allow-Credentials這個響應頭并不能被緩存?
問題:
腳本會發送跨域請求,Origin指向的是HTML所在源還是腳本所在源呢?
參考:
總結
以上是生活随笔為你收集整理的CORS(跨域资源共享)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阅读react-redux源码 - 零
- 下一篇: Access-Ctrol-Allow-H