為什么有了跨域這個東西
世上本沒有路,走的人多了也就有了路。 跨域這算是這么一回事。 在 Web 的世界上本沒有跨域這個東西,但架不住壞人越來越多,所以后來就有了跨域。
何出此言?
互聯網剛出現的時候,是默認全開放的。也就是你說,你寫的一個網站默認是允許所有人訪問的。這一點非常符合互聯網"互聯互通"的精神本質。但后來人們卻發現,事情好像有點變質變味。
比如說,Bob 寫了一個網站讓 Alice 訪問,當 Alice 訪問這個網站的時候,誰知 Eve 中途攔截了請求,在里面植入了 Eve 自己寫的 Js 腳本。 這個腳本會源源不斷的請求 Bob 網站的數據,并且通過瀏覽器發給 Eve。
Eve 的腳本此時此刻充當了小偷的角色,盜取數據并返回給 Eve。
既然第三方的 Js 有這樣的危險,那么我們對這些 Js 腳本禁止執行怎么樣? 必須不能! 你想想互聯網中除了有壞人,也有好人呀。如果我們禁止所有 Js 腳本運行的話,那我們就無法使用網銀了,電商平臺了,甚至連絢麗多彩的 Web UI 都無法看到了。
既然不能禁止,也不能放任自流,那又該何去何從?所以引出了跨域,跨域簡而言之,就是瀏覽器在發請求的時候,先詢問一下服務器,你讓我能做點啥?
為什么是瀏覽器
這是一個好主意,為什么跨域只出現在瀏覽器呢? 這也算是當前一個無解的事情,因為 Http 請求天生是無狀態的,服務器無法區分這個請求和上個請求是否是同一個客戶端發起。當無法判斷是否為同一個客戶端時,服務端也就無法判斷這個請求是應該通過還是應該禁止。
所以跨域這件事只能下沉到瀏覽器來執行了。讓瀏覽器承擔第一道防火墻的責任。 畢竟能做瀏覽器的都是大廠,這點基本職業操守,瀏覽器廠商還是有的。
那話說回來,服務端就一點也不做么? 并不是的, 服務端限制來自服務端請求有另外方案,比如 Token、API Check 等方式。
因為 RestFul API 之間無狀態,所以只能通過校驗請求合法性,來變相控制對方是否有權限獲取/變更數據。
瀏覽器如何實現跨域
瀏覽器通過協議+域名+端口這三者來判斷是否屬于跨域。 整理如下:
瀏覽器地址 |
準備訪問地址 |
是否跨域 |
原因 |
http://www.bob.com |
http://www.aclice.com |
跨域 |
域名不一致 |
http://www.bob.com |
https://www.aclice.com |
跨域 |
協議、域名不一致 |
http://www.bob.com |
https://www.bob.com |
跨域 |
協議不一致 |
http://www.bob.com |
http://www.bob.com:8080 |
跨域 |
端口不一致 |
http://www.bob.com |
http://www.bob.com |
不跨域 |
|
瀏覽器每次在發起請求的時候,都會經過這個邏輯判斷。 如果判斷需要跨域,那么就會先向服務端發起OPTIONS請求。OPTIONS請求是向服務器詢問,你允許哪些外部域名訪問你?如果服務端允許當前瀏覽器訪問,瀏覽器再繼續訪問。如果不允許,那就此作罷。
如圖所示, 瀏覽器通過 OPTIONS 請求詢問服務端。 服務端回復只接受來自https://www.bob.com的請求。
如果當前瀏覽器是https://www.bob.com那么愉快的發起正式請求,如果瀏覽器不是https://www.bob.com。那么就此別過以后再見。
是不是感覺跨域好像很簡單,也沒有多么復雜。 事實也是這樣的,跨域本身不復雜,復雜的里面的參數配置。
常見的參數配置
前面說過了,跨域的控制權限在服務端,不在客戶端。 因此服務端如果需要支持跨域,那么需要將以下參數通過 OPTIONS 請求返回給客戶端。
- Access-Control-Allow-Origin: <URI>| *
用來標識誰可以訪問我。 這個 header 只支持返回一個值,要么是特定外部 URI(https://www.devexp.cn),要么是"*"。 不接受其他值 如果服務端指定了具體的域名而非“*”,那么響應首部中的 Vary 字段的值必須包含 Origin。這將告訴客戶端:服務器對不同的源站返回不同的內容。
- Access-Control-Expose-Headers: 允許瀏覽器可以訪問哪些自定義 Header
默認情況下,瀏覽器只能讀取最基本的響應頭,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。 如果服務端想讓瀏覽器獲取其他 Header,就需要通過這個 Header 來指定。 這個 Header 支持逗號分隔的多值
- Access-Control-Max-Age: 預檢請求的結果在多少秒內有效
如果每次請求都需要預檢,那么瀏覽器工作壓力大,而且也浪費帶寬。所以服務端可以通過這個 Header 告訴瀏覽器,多長時間內免檢。
- Access-Control-Allow-Credentials: 是否允許客戶端攜帶驗證信息
默認客戶端只能攜帶特定的 header 發給服務端,如果服務端允許驗證身份信息的 header(Cookie)。那么通過將此 Header 置為 true 來解決此問題。
當
Access-Control-Allow-Credentials: true 時,其他控制 Header 就不能是"*"了。比如:
- 服務器不能將 Access-Control-Allow-Origin 的值設為通配符“_”,而應將其設置為特定的域,如:Access-Control-Allow-Origin: https://example.com。
- 服務器不能將 Access-Control-Allow-Headers 的值設為通配符“_”,而應將其設置為首部名稱的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
- 服務器不能將 Access-Control-Allow-Methods 的值設為通配符“*”,而應將其設置為特定請求方法名稱的列表,如:Access-Control-Allow-Methods: POST, GET
- Access-Control-Allow-Methods: 表示實際請求可以是哪些請求
這個 Header 接受逗號分隔的多值
- Access-Control-Allow-Headers: 表示實際請求可以攜帶的 Header
這個 Header 接受逗號分隔的多值
"豁免"的情況
不知為何,瀏覽器會豁免一些場景的跨域判斷。 被豁免的場景稱之為"簡單請求",不被豁免的則稱為"復雜請求"。
符合下面條件的就是簡單請求:
- 是 GET、HEAD、POST 之一
- 只有 Accept、Accept-Language、Content-Language、Content-Type 這幾個 Header
- Content-Type 的值僅限于下列三者之一:text/plain、multipart/form-data、Application/x-www-form-urlencoded
除此之外的請求都需要經過預檢判斷。
常見問題
- 配置了跨域,瀏覽器仍然顯示跨域錯誤?
首先判斷是否滿足跨域判斷,然后檢查服務端返回的
Access-Control-Allow-Origin、Access-Control-Allow-Methods 和 Access-Control-Allow-Headers 是實際發起的值是否匹配。
- OPTIONS 請求返回 204 還是 200?
這個問題仁者見仁,智者見智。 但目前絕大多數的配置指南都建議返回 204. 因為從語義上說 204 表示請求成功,但沒有任何返回。所以比較適合 OPTIONS 請求場景。