雙17期|跨域資源共享:跨域錯誤該前端還是後端處理?

語言: CN / TW / HK
ead>

theme: channing-cyan highlight: a11y-dark


持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第7天,點選檢視活動詳情

雙17期|跨域資源共享:跨域錯誤該前端還是後端處理?

tips:每個技術點都值得優學優寫:17期

一、寫在前面

導讀:本文旨在講清楚跨域資源共享(CORS)的由來,CORS 的一些限制,以及解除這些限制的方法。不重在提供更多的跨域解決方案,儘管一些同學更喜歡上來就要答案,而不是看推演和分析。當然文末也附加了一些跨域解決方案。

XHR 是一個瀏覽器層面的的 API,我們使用起來很簡便,但其實它的內部隱藏了大量的底層處理,例如重定向、快取、認證相關,以及內容協商等,算是內部高度封裝隱藏,對外暴露少量的一種情況。

當然,也正因如此,使得:

  • ①XHR的API使用變得更簡易,人們能夠把更多的精力放在業務實現上。

  • ②這樣使得應用程式碼得到了瀏覽器沙箱機制施加的安全保護,這種保護體現在安全限制上。

瀏覽器沙箱中,有一種情形是同源沙箱,它不允許 hello.com 中指令碼訪問操作 word.com 中的使用者資料。

事實上,早起的 XHR 都限制應用只能執行同源請求,就說是在 hello.com 中只能訪問請求 hello.com 中的資源,如果不同源,瀏覽器就會拒絕該 XHR 請求並報錯。

為什麼不提 axios,ajax 等支援非同步請求的外掛,老提已經很少用的 XHR,XHR 並沒有過時,也不是很少被使用, 只是不在明面而已,而且早期那時還沒 axios,ajax 啥事,並且這兩的本質也都是 XHR,看下瀏覽器的控制,可以感受下 XHR 和 axios,ajax的關係。當你發起一個 axios 請求時,你想看它的請求情況,點開選項是 XHR 還是啥呢?

image.png

二、同源與跨域資源共享(CORS)

  • ①什麼是同源

此時,我們引入了一個同源的概念,那麼什麼是“源”呢?清楚“源”的組成成分,才能很好的區分是否同源。

一個“源”由於應用協議、域名、和埠號共同組成。所以,只有這三者都一樣才能算是同源,三者中只要有一方不同,都是非同源。

  • ②為什麼要有同源策略?

同源策略的出發點:一些敏感的資料被瀏覽器儲存,例如使用者相關資料、認證令牌、cookie等。這些資料不應被洩露給其他應用(其他源),同源策略正是處於保護資料不被洩露的目的而設定的。它禁止了不同源的指令碼相互訪問和操作資料。

  • ③同源策略的限制和跨域資源共享

同源策略提供了安全保護,但有時也會帶來麻煩,因為同源策略限制了不同源之間資源的置換。這就引出了跨域資源共享(Cross-Origin Resource Sharing,CORS)。CORS 策略使得不同源之間發起請求得以實現,但是 CORS 設定前置條件:選擇同意機制。當然這個選擇同意機制由瀏覽器底層處理。

該選擇同意機制的處理邏輯大概是:瀏覽器對發起的請求,自動追加包含著請求來源的受保護的 Origin HTTP 首部,而伺服器具有檢查請求的 Origin 首部,選擇是否接收該請求的權利,同意接收請求則返回 Access-Control-Allow-Origin 相應首部,不同意則在響應中不包含 Access-Control-Allow-Origin 相應首部即可。伺服器同意之後就實現了跨域資源共享,所以解決跨域問題,實現跨域資源共享的常規辦法之一,就是服務端的同學對跨域請求設定同意,響應中設定 Access-Control-Allow-Origin 相應首部。

如果你在使用一些非同步請求時足夠仔細,那麼可能會發現下面的資訊:

image.png

tips:正如上圖所示,CORS 允許伺服器設定 Access-Control-Allow-Origin:*,這個 * 是一個通配值,表示允> 許和同意接收來自任何“源”的請求。這種寫法應該謹慎使用,更合適的寫法應該是寫法具體的“源”,例如: Access-Control-Allow-Origin:http:hello.com

  • ④跨域資源共享的其他內容

CORS 為確保伺服器支援它,會採取一些列措施,例如:

①CORS 請求預設會省略 HTTP 認證和 cookie 等使用者憑證。

②瀏覽器會限制 CORS 請求只能傳送 head、get、post特定方法的請求。(所以注意的話,有時可能會遇到 put 請求會觸發跨域報錯,而一些 get請求則不會,這在 ie 中可能更能體現。)

③只能訪問可以通過 XHR 傳送並讀取的 HTTP 首部。

那麼如何讓 CORS 請求支援 cookie 和 HTTP 認證呢?

回答:客戶端在傳送非同步請求時,設定 withCredentials 為 true。意味跨域請求是否使用憑證。同時伺服器也必須應向適當的首部(Access-Control-Allow-Credentials),意味同意使用憑證,允許應用程式傳送使用者的隱私資料。所以一些同學可能會發現 axios 要想支援豐富的跨域請求就得設定 withCredentials。就像下面這樣:

js // 建立axios例項 const service = axios.create({ headers: { 'token': '' }, baseURL: '', withCredentials: true // 表示跨域請求時是否需要使用憑證,開啟後,後端伺服器要設定允許開啟 })

需要注意的是前端應用設定了 withCredentials,後端介面需要配合設定 Access-Control-Allow-Credentials 才有效。

那麼如何讓 CORS 請求支援客戶端寫或者讀自定義的 HTTP 首部,或者支援使用 delete、put 等“不簡單”的請求方法?

回答:所以有時會發現,明明瀏覽器控制檯能看到響應中包含有自定義的 HTTP 首部(請求頭欄位),但是前端卻讀取不到,這都是 CORS 的選擇同意機制的限制。要想實現這些,需要服務端的配合。

預設 reponse header 只能取到 Content-Language,Content-Type,Expires,Last-Modified,Pragma ,5個預設值,要想取得其他的自定義欄位需要在服務端設定 Access-Control-Expose-Headers,配置前端想要獲取的 header,比如自定義的 filename,需要在後端做類似下面這樣的設定。

image.png

還有一點就是需要向伺服器傳送預備(preflight)請求。當然這一步通常是底層處理,不需要程式設計師處理。

三、寫在後面

tips ①:W3C 的 CORS 規範規定了:“簡單的”請求可以跳過傳送預備(preflight)請求,但其他“不簡單的”請求都需要傳送預備請求。毫無疑問這種預備請求驗證會帶來延遲問題,好在是完成預備請求,瀏覽器就會將結果快取起來,後續請求就不必重複驗證了。

tips ②:瞭解更多的 CORS 策略和實現,請參考 W3C 官方標準。

tips ③:所以,跨域問題該由前端還是後端處理?看懂 CORS 的選擇同意機制,應該都知道答案了,那就是需要前後端共同處理或都可以處理, 沒有限定一方。是協同處理還是一方處理,要看選用的解決方案,比如常規解決跨域資源共享,那就得前後端協同配合, 如果另闢蹊徑對瀏覽器規避“非同源”和 XHR,例如利用 nginx 反向代理那就是前後端都可以,看誰管理 nginx 了。

實踐中有一些辦法可以“欺騙”瀏覽器,“欺騙”瀏覽器沒有執行非同源的請求(跨域請求),儘管在結果上就是進行了跨域資源共享。 但是由於成功“欺騙”了瀏覽器,導致沒觸發 CORS 相關的策略,比如藉助 nginx 的代理轉發功能,把跨源這一步轉移到了 nginx 中, 在瀏覽器端表現的是同源請求。這種手段另闢蹊徑,繞過了 CORS 的選擇同意機制,因為瀏覽器認為那是同源請求, 他們把跨源這一步移交給了 nginx 處理。vue-cli 中的 proxy 也是這個道理吧?

tips ④:注意,nginx 沒有 CORS 策略,同樣的 postman 和 node.js 也沒有。所以有時可能會發現後端的同學說, 他們在 postman 或 同源的 swagger 上測試了介面沒問題,沒發生跨域錯誤。相信真正瞭解 CORS 的同學不會以此為依據, 去證明跨域有沒有被正確處理,因為那毫無說服力。

思考,為什麼解釋 CORS 要從 XHR 入手開始,因為從實踐來看他們之間有著極大的聯絡, 不過我暫時沒有證據證實 CORS 的作用範圍僅限於瀏覽器之XHR,那麼為什麼下面通過 script、img 和 iframe 的 src 請求非同源資源不會發生跨域錯誤, 而通過 axios(本質是 XHR)會發生跨域錯誤呢?JSONP 不正是利用了 script src 這一特點嗎?如果你感興趣, 可以通過下面兩種方式請求非同源資源去驗證。

```html

cors demo

script src 請求非同源資源,不會發生跨域錯誤

```

image.png

四、其他附加的跨域解決參考方案

①vue 開發環境使用 vue-cli 中的 proxy 處理。

vue-cli 3+ 中

js proxy: { '/api': { // 對 '/api' 開頭的介面地址進行代理 target: 'http://www.example.org', // target host 目標地址(通常是介面地址) changeOrigin: true, // needed for virtual hosted sites ws: true, // proxy websockets // 代理websockets pathRewrite: { '^/api/old-path': '/api/new-path', // rewrite path // 重寫路徑 '^/api/remove/path': '/path', // remove base path // 移除基礎路徑 }, router: { // when request.headers.host == 'dev.localhost:3000', // override target 'http://www.example.org' to 'http://localhost:8000' // 當請求頭的host地址為dev.localhost:3000時, // 將目標地址'http://www.example.org'重寫為'http://localhost:8000' 'dev.localhost:3000': 'http://localhost:8000', }, }, '/foo': { // 對 '/foo' 開頭的介面地址進行代理 target: '<other_url>' } }

vue-cli 2中 js proxyTable: { '/api': { target: 'http://14.11.11.11:8999/', // localhost=>target 目標介面地址 changeOrigin: true, pathRewrite: { '^/api': '/' // 路徑重寫 } } },

② nginx 代理轉發請求到不同源示例

```nginx

tomcat專案 外部訪問地址:域名/

location /{
proxy_pass http://location:8088 }

nginx專案 外部訪問地址:域名/wx/

location /wx/{ proxy_pass http://location:8081 } ```

③ jsonp 技術(實際專案中選用過)。

大多通過 nginx 代理解決或前後端配合解決

④ window.postMessage(實際專案中未選用過)