從網路鏈路到跨域問題

Surmon發表於2017-04-21

首先,得知道什麼是域。

再首先,得先知道Web服務從訪問到收到資料並展現這個過程發生了什麼。

IP

注:下文中 網路空間 = TCP/IP網路空間, IP = IP地址

IP本身是Internet Protocol的縮寫,是一種為了計算機相互連線通訊而產生的協議,我們在這就用它代指ip地址。

每個線上的網路服務在網路空間的真實存在形式都是以ip地址形式存在的,它屬於網路七層模型中的網路層,它是一個地址,用來識別網路空間中互聯的主機和路由器,暫且認為用ip地址可以訪問到一個伺服器。

好比地球上的每個地點都有一個經緯度,只要這個經緯度真實存在並有效可用,我們根據這個經緯度就一定能找到這個地點,且經緯度是永久不變的。

實際上ip地址的“永久不變”是指在這個ip對應的網際網路服務的生命週期內,其不會變化。

直接服務場景:假設某公司拉了一條電信專線服務,電信給其分配8個ip地址,其中3個為廣播地址,剩餘5個配置且部署好對應的網路服務,外部是可以直接訪問到該公司所架構的網路服務的。

真實商業生產場景:假設在某雲服務廠商購買伺服器,預設會分配有唯一ip,伺服器重啟、重置...ip是不會變化的,但伺服器如果到期了,伺服器提供商會同時釋放掉ip資源,這個ip可能就會分配給其他服務,亦或者回收這個ip,ip本身和服務沒有關係,但大多數的商業服務ip都是隨服務捆綁的。

簡單說:ip是指向網路空間具體某一處的唯一地址,但它並不是永遠不變的。

域名和DNS

像經緯度一樣,ip地址是一長串數字,不便於記憶,所以我們需要一個類似地名一樣的別名,當我們一旦說出別名,就知道它大概在哪,且無需care它的真實經緯度。

我們之所以聽到地名就知道這個位置大概所在,是因為我們大腦內已經儲存了這個地名和真實地點的關聯資訊,暫且稱之為我們儲存的這塊用於關聯地點的資料為“資料庫”。

人腦有限,我們不可能記住所有的地名和地理位置,所以需要有一個容易專門來儲存這些關聯資料的資料庫,最好其可以直接把我們帶到目的地。

這就是DNS,全稱Domain Name System,他做的事情很簡單,就是將我們輸入的ip別名(域名)通過資料庫解析為ip地址返回。

實際上,瀏覽器或我們發起請求的客戶端會根據DNS返回的ip去請求網路資源,並返回解析展示。

然而,瀏覽器請求時如何知道域名使用的是哪家DNS服務商呢,於是瀏覽器便需要先把域名傳送到本地配置的DNS服務商(本地域名伺服器/Local DNS Server)那裡得到該域名的NS(Name Server),然後再把該域名拿到Name Server去獲取IP,然後再向該IP請求資料。

實際上整個域名解析的鏈路十幾步不止,瀏覽器請求LDNS之前會對瀏覽器本身的DNS快取和本機DNS快取的判斷,判斷會根據命中情況和TTL和其他資料決定是否進入下一步。

LDNS如果查詢失敗,則會直接請求root DNS Servers,root DNS Servers只為全球只有十三臺的gTLD(generic Top-Level DNS Server)進行服務,RDNS會返回域名所在的主域名伺服器的地址,即對應的gTLD地址,然後繼續向gTLD傳送請求以得到NS地址,於是又回到了上一步。

一張圖來表示:

從網路鏈路到跨域問題

簡單說就是:DNS是一套完整的系統,這套系統做的事就是根據一個個的表去查對應的資料,最終返回一個目標IP。

跨域

該說域了;我們可以把域理解為一個域名或IP所代表的範圍,比如訪問a.com指向了A伺服器,訪問b.com指向了B伺服器,我們可以認為a和b是分開獨立的兩個域。

大多數情況下,a.com b.com都應該是兩個沒有關係的單獨網路服務,他們預設不應該產生關聯(靜態資源引用除外),起碼瀏覽器是這麼認為的。

所以如果你在a.com下向b.com發起一個觸發瀏覽器安全機制的xhr的網路請求,瀏覽器預設是會攔截的,並附贈一大串Error,他認為你這麼做不安全,不允許跨域請求資料。

CORS

CORS(Cross-origin resource sharing)是一個W3C標準,全稱"跨域資源共享"。
它規定允許瀏覽器向跨源伺服器,發出XMLHttpRequest請求,從而解決跨域問題。

我們先看瀏覽器的安全機制是怎樣的?

瀏覽器把非同步請求(xhr/fetch)分為兩類:

  • 簡單請求
  • 非簡單請求

簡單請求的條件:

  1. 請求方法是以下三種方法之一:

    • HEAD
    • GET
    • POST
  2. HTTP的頭資訊不超出以下幾種欄位:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限於三個值application/x-www-form-urlencodedmultipart/form-datatext/plain

只要不滿足以上條件的請求均為非簡單請求

簡單請求

簡單請求在發出時,瀏覽器會自動給請求頭加上Origin欄位,該欄位為當前請求發出者所在的域(協議 + 域名 + 埠),
服務端根據請求headers裡的Origin來判斷是否允許請求者獲取資源,如果允許,服務端在返回時會在headers裡攜帶幾個特殊的欄位,用於告知瀏覽器,允許此次請求,
否則,即使正常返回資料,瀏覽器檢測到無對應的允許跨域欄位,也會在console throw Error,告知你跨域訪問失敗。

這幾個欄位便是CORS標準中所實現的三個欄位:

  1. Access-Control-Allow-Origin
    該欄位是必須的。它的值要麼是請求時Origin欄位的值,要麼是一個*,表示接受任意域名的請求。

  2. Access-Control-Allow-Credentials
    該欄位可選。它的值是一個布林值,表示是否允許傳送Cookie。預設情況下,Cookie不包括在CORS請求之中。設為true,即表示伺服器明確許可,Cookie可以包含在請求中,一起發給伺服器。這個值也只能設為true,如果伺服器不要瀏覽器傳送Cookie,刪除該欄位即可。
    如果要傳送Cookie,Access-Control-Allow-Origin就不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用伺服器域名設定的Cookie才會上傳,其他域名的Cookie並不會上傳,且(跨源)原網頁程式碼中的document.cookie也無法讀取伺服器域名下的Cookie。

  3. Access-Control-Expose-Headers
    該欄位可選。CORS請求時,XMLHttpRequest物件的getResponseHeader()方法只能拿到6個基本欄位:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他欄位,就必須在Access-Control-Expose-Headers裡指定。

非簡單請求

非簡單請求一般出現在對服務端進行CUD操作的場景下,異源RESTful就是一種最典型的場景,會使用到PUTDELETEPATCH...等請求型別,且一般以application/json格式進行資料互動。

當瀏覽器把一個請求判定為非簡單請求,則其發出前,瀏覽器會預先對服務端發起一個OPTIONS型別的預檢(preflight)請求,同時也會加上Origin欄位,
瀏覽器會根據此次預檢請求返回的響應頭來判斷,服務端是否允許本域跨域操作資源,若判斷為允許,則瀏覽器立即發出本身要發出的請求,否則,控制檯丟擲異常,中斷請求。

服務端應返回的響應頭應包含以下幾個CORS欄位:

  1. Access-Control-Allow-Origin
    必需返回,同簡單請求中的含義。

  2. Access-Control-Allow-Methods
    該欄位必需,它的值是逗號分隔的一個字串,表明伺服器支援的所有跨域請求的方法。注意,返回的是所有支援的方法,而不單是瀏覽器請求的那個方法。這是為了避免多次"預檢"請求。

  3. Access-Control-Allow-Headers
    如果瀏覽器請求包括Access-Control-Request-Headers欄位,則Access-Control-Allow-Headers欄位是必需的。它也是一個逗號分隔的字串,表明伺服器支援的所有頭資訊欄位,不限於瀏覽器在"預檢"中請求的欄位。

  4. Access-Control-Allow-Credentials
    可選返回,同簡單請求中的含義。

  5. Access-Control-Max-Age
    該欄位可選,用來指定本次預檢請求的有效期,單位為秒。上面結果中,有效期是20天(1728000秒),即允許快取該條迴應1728000秒(即20天),在此期間,不用發出另一條預檢請求。

在每一次真正的資料請求時,Access-Control-Allow-Origin欄位都是服務端一定會返回的。

為避免頻繁的預檢請求降低效率,真實生產環境下,建議設定Access-Control-Max-Age欄位。

解決方法

開發環境下解決方案

通過本機開啟代理實現在開發環境下將api轉化為子路徑,Node.js、apache、nginx均可實現。

webpack版本程式碼: 全部程式碼

proxy: {
    '/api': {
        target: 'http://localhost:8000',
        secure: false,
        changeOrigin: true,
        pathRewrite: {
            '^/api': ''
        }
    }
},複製程式碼

生產環境下解決方法

為服務端設定預檢請求的響應及相關的CORS欄位。Node.js版本程式碼

誤區

JSONP只是在CORS未規範之前用於解決基本跨域的曲徑(奇技淫巧),其並非解決跨域的真正途徑。

web開發者在使用vue-resource、axios...等各種非同步庫時,可能會存在類似解決跨域的選項,其可能是內部對簡單請求和非簡單請求進行的一些基本轉換,此類並非真正解決跨域的方法,解決跨域務必需要服務端處理。

本文部分內容參考來源:

《跨域資源共享 CORS 詳解》

MDN - HTTP訪問控制(CORS)

W3C - HTTP/1.1: Method Definitions

原文地址:surmon.me/article/21

相關文章