No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」

什麼是跨域 ?

跨域這個問題大家並不陌生,這也是面試的高頻問題,很多人都背過,什麼因為同源策略啊,CORS 啊等等,跨域的標緻就是瀏覽器控制檯出現 Access to XMLHttpRequest at 'https://xxx.xxx.com' from origin 'https://xxx.xxx.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.。

首先,只有 Web 瀏覽器才會產生跨域,這是因為瀏覽器的同源策略在限制。同源策略就是 [域名(又稱為主機 host),端口(port),協議(protocol)] 要統一,否則就會被瀏覽器判定為跨域,然後拒絕請求。但是嚴格的說,瀏覽器並不是拒絕所有的跨域請求,實際上拒絕的是跨域的讀操作。

同源策略並不是不好,它在一定程度上保證了瀏覽器的安全,只是無法適應現代的潮流,在工程服務化後,不同職責的服務分散在不同的工程中,往往這些工程的域名是不同的,但一個需求可能需要對應到多個服務,這時便需要調用不同服務的接口。


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


哪些情況下會產生跨域 ?

No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」

什麼是 CORS ?

跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的 Web 應用被准許訪問來自不同源服務器上的指定的資源。當一個資源從與該資源本身所在的服務器不同的域、協議或端口請求一個資源時,資源會發起一個跨域 HTTP 請求。 比如,站點 http://domain-a.com 的某 HTML 頁面通過 的 src 請求 http://domain-b.com/image.jpg。網絡上的許多頁面都會加載來自不同域的 CSS 樣式表,圖像和腳本等資源。 出於安全原因,瀏覽器限制從腳本內發起的跨源 HTTP 請求。 例如,XMLHttpRequest 和 Fetch API 遵循同源策略。 這意味著使用這些 API 的 Web 應用程序只能從加載應用程序的同一個域請求 HTTP 資源,除非響應報文包含了正確 CORS 響應頭。


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


跨域資源共享( CORS )機制允許 Web 應用服務器進行跨域訪問控制,從而使跨域數據傳輸得以安全進行。現代瀏覽器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch )使用 CORS,以降低跨域 HTTP 請求所帶來的風險。

不過呢,並不一定是瀏覽器限制了發起跨站請求,也可能是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。

在“上古時代”,解決跨域有很多黑科技,什麼 JSONP 啊,window.name 啊,document.domain 啊等等都用上了,但現代用的基本都是CORS跨域,所以上述方法在這裡就不討論了。

由上述可知,實現 CORS 通信的關鍵是服務器。只要服務器實現了 CORS 接口,就可以跨源通信。

雖然工作重心在後端,但是作為前端,也要了解這方面的知識,否則就會出現204預請求,然後後端沒處理,就甩鍋給前端的情況。

下面介紹一些 CORS 過程中常見的 HTTP 響應頭(Response Headers)。

什麼是 Access-Control-Allow-Origin ?

這是 HTTP 響應首部中的一個字段,

具體格式是: Access-Control-Allow-Origin: <origin> | *。/<origin>

其中,origin 參數的值指定了允許訪問該資源的外域 URI。對於不需要攜帶身份憑證的請求,服務器可以指定該字段的值為通配符,表示允許來自所有域的請求。

如果服務端指定了具體的域名而非*,那麼響應首部中的 Vary 字段的值必須包含 Origin。這將告訴客戶端:服務器對不同的源站返回不同的內容。例如:

<code>// 只響應來自 http://mozilla.com 的請求
Access-Control-Allow-Origin: http://mozilla.com/<code>

什麼是 Access-Control-Allow-Methods ?

該字段必需,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請求的那個方法。這是為了避免多次"預檢"請求。

具體格式是:Access-Control-Allow-Methods: <method>[, <method>]*/<method>/<method>

什麼是 Access-Control-Allow-Headers ?

可支持的請求首部名字。請求頭會列出所有支持的首部列表,用逗號隔開。

如果瀏覽器請求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限於瀏覽器在"預檢"中請求的字段。

具體格式是:Access-Control-Allow-Headers: <header-name>[, <header-name>]*/<header-name>/<header-name>

什麼是 Access-Control-Allow-Credentials ?

該字段可選。它的值是一個布爾值,表示是否允許發送 Cookie。默認情況下,Cookie 不包括在 CORS 請求之中。設為 true,即表示服務器明確許可,Cookie 可以包含在請求中,一起發給服務器。

瀏覽器的正常請求和回應

一旦服務器通過了"預檢"請求,以後每次瀏覽器正常的 CORS 請求,就都跟簡單請求一樣,會有一個 Origin 頭信息字段。服務器的回應,也都會有一個 Access-Control-Allow-Origin 頭信息字段。

好了,鋪墊完成,接下來要說說我踩的坑了。


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


問題實記

項目背景

公司因歷史遺留,同時存在3種後端語言:Java,PHP,Node.js,因為後端同事都不懂 Node.js,所以 Node 項目一直是前端維護。老項目用的是 Koa 框架,之前我在上面寫邏輯,上傳圖片是沒有問題的,直到我用了 Nest 框架重構,將後臺管理系統的邏輯拆分解耦出來。

踩坑過程

新項目一切請求都正常,本地跑的時候也正常,但是到了線上,只有上傳圖片不正常。每次上傳都會預請求一次 204 OPTIONS,這一步沒問題,但接下來的 POST 請求就有問題了:


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


我一看控制檯的信息,結合多年的開發經驗(其實並沒有),這不就是跨域嘛,於是看代碼,main.ts 中已經有了 app.enableCors(),百思不得其解,於是 Google,發現也有人遇到過類似的問題,說是 Nest 某些版本在線上環境無法正確使用 CORS,然後就形成了下列代碼:


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


但是,並沒有什麼卵用,調試了好一會兒才注意到響應頭是 nginx 返回的,然後就像發現新大陸一樣屁顛屁顛的找運維老大:代碼裡面已經設置了 CORS 跨域了,是不是被 nginx 攔截了?(公司的服務器通過 nginx 做負載均衡,然後才到 node)

然後運維老大很配合的幫我配置了 nginx 的跨域,然後就悲劇了,連 OPTIONS 都過不去:


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


控制檯的大致意思是 CORS 規則衝突了,只能使用一種。


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


所以這個問題其實和 nginx 沒有什麼關係,運維大佬看了訪問日誌,說是 OPTIONS 請求是響應了的,但是到 POST 請求的時候就斷開連接了,然後讓我試試直接訪問端口,於是控制檯又出現瞭如下信息:


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


唔,大致意思是,源頭是 https 協議的話,就不能請求 http 協議的資源。

於是我把網站上的 https 換成 http,就出現瞭如下信息:


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


我就覺得很奇怪,因為本地開發的時候,是能正常上傳圖片的。唯一不同的地方就在於,線上使用的是 pm2 進程管理工具,而我本地用的是自帶的 nodemon。為了驗證我的猜測,於是自己的電腦上也裝了 pm2,然後跑起來,然後就。。。Bug 果然復現了。

於是讓運維大佬不用 pm2 直接用 nodemon 啟動試試,然後折騰我很久的跨域問題就“解決了”。為什麼打引號呢,因為運維大佬並不是很想用 nodemon 來管理進程,主要是如果服務器宕機,不能自動拉起,戰役還遠沒有結束。

然後我就圍繞著 pm2 繼續探索,把官方文檔看了個遍,也沒找到關鍵點,鬱悶之下,只好檢查的 pm2 啟動配置,然後注意到這個:

No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」

這是當初我為了輸出日誌的時候,更新了文件,防止被 pm2 監聽到,導致服務一直重啟所做的措施。然後就想到了,我上傳圖片的時候,是先在硬盤保存,然後讀取 Buffer 流,然後再上傳到 oss,最後刪掉硬盤的圖片,核心代碼如下:

No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


所以我就在想,是不是因為上傳的時候,存本地的圖片觸發了 pm2 的監聽,導致服務重啟,所以就會報 net::ERR_CONNECTION_RESET,於是我改成了臨時目錄 const uploadCachePath = '/tmp/assets/uploads'; (Mac OS、Linux 都有這個目錄),然後 pm2 啟動,上傳,成功。


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


至此,折磨了我近一週的 Bug 終於修復,結果和跨域沒有半毛錢關係。

No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」

插曲

有讀者可能注意到 /tmp/assets/uploads 路徑,要是同事用的是 Windows 系統開發咋辦?這個我自然也想到了,於是改了 pm2 的啟動項:


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」

可是無論怎樣改,依然會觸發上述 Bug,因為服務器同時跑著 2 個 Node 項目,另一個項目也有自己的 pm2 啟動項,所以感覺這個配置被另一個覆蓋了,pm2 似乎是全局的。如果有其他大神深入瞭解過 pm2 的可以指點一下。

所以和運維大佬討論了一下,給我開了權限,就暫時用這個臨時目錄,待以後找到更好的解決方案再優化,反正目前這個項目也只有我一人在維護。

遇到的其他場景

1. 服務器宕機

就在我剛找到解決方案的時候,我帶的小弟跑過來問我是不是動了配置文件,老項目怎麼都跨域了。我去看他的控制檯,確實有 Access to ... has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 的信息,由於剛踩的坑,何況我也沒動過配置文件,所以覺得這肯定不是跨域問題。然後我看了服務器的日誌,發現一直在重啟,因為有個模塊他沒同步上去,只同步了路由文件,導致路由找不到對應的函數,服務就一直在報錯重啟,根本就沒處理請求。於是讓他把代碼重新上傳,問題解決。

所以個人猜測(因為沒有權限看服務器的配置),這次跨域是 nginx 代理的時候,因為訪問不到 Node 服務,所以自然而然地就讀不到 CORS 配置,然後報跨域錯誤。

2. Axios 的自行檢查

Axios 創建實例時,有個字段需要注意:

No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」

如果不設為 false,則會得到下面報錯:

No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」

原因就是前面提到的 Access-Control-Allow-Credentials 字段,CORS 請求默認不發送 Cookie 和 HTTP 認證信息。如果要把 Cookie 發到服務器,一方面要服務器同意:

Access-Control-Allow-Credentials: true

另一方面,開發者必須在 AJAX 請求中打開 withCredentials 屬性,也就是 Axios 默認打開的 withCredentials: true。

否則,即使服務器同意發送 Cookie,瀏覽器也不會發送。或者,服務器要求設置 Cookie,瀏覽器也不會處理。

但是,如果省略withCredentials設置,有的瀏覽器還是會一起發送 Cookie。這時,需要顯示關閉 withCredentials: false。

需要注意的是,如果要發送 Cookie,Access-Control-Allow-Origin 就不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie 依然遵循同源政策,只有用服務器域名設置的 Cookie 才會上傳,其他域名的 Cookie 並不會上傳,且(跨源)原網頁代碼中的 document.cookie 也無法讀取服務器域名下的 Cookie。

總結

由上述可以總結,在後端配置了 CORS 的情況下,還會造成 Access to ... has been blocked by CORS policy ... 的情況大致有:

  1. 服務器突然重啟,導致代理服務器轉發中斷;
  2. 服務器宕機,導致代理服務器讀不到 CORS 配置;
  3. Axios 等請求插件設置了withCredentials: true,導致先行驗證並攔截了請求;

有些時候,瀏覽器控制檯給出的錯誤信息,不一定能真正地指出問題的所在,做為前端,還需要多去了解一些更本質的東西。


No 'Access-Control-Allow-Origin' header跨域問題「踩坑記」


分享到:


相關文章: