如果在開發、建置網站的過程中,有嘗試透過任何套件、框架,或是瀏覽器的 fetchXHR 串接外部 API,相信對於「跨域(Cross-Origin)請求」這個名詞一定不會陌生,更別說是那個怵目驚心的 CORS(Cross-Origin Resource Sharing)錯誤訊息。

今天就來討論跨域問題的原因以及解決方法。

跨域請求#

如果前述的狀況沒有遇過,可以嘗試在瀏覽器 console 頁輸入下列的程式碼:

const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4) {
    console.log(xhr.status === 200 ? xhr.responseText : "error");
  }
};
xhr.open("GET", "https://google.com");
xhr.send();

這段程式碼透過瀏覽器的 XMLHttpRequest,對 Google 送出 Request,會得到跨域請求問題。

當開發者透過 JavaScript 針對不同於當前位置的來源發送請求,這個請求的回應就會被瀏覽器攔截掉,不交給 JavaScript 處理。

這邊的「不同來源」,指的是目標資源與當前網頁的網域(domain)、通訊協定(protocol)或通訊埠(port)只要有任一項不同,就算是不同來源。例如以下幾個例子:

假設目前使用者在:https://example.com :

[O] https://example.com/test -> 同域
[X] https://m.example.com -> 網域不同
[X] https://example.com:3000 -> PORT 不同
[X] http://example.com -> 通訊協定不同

理解什麼是跨域了,那為什麼瀏覽器要這麼雞婆把跨域請求資源擋掉?這是基於同源政策(Same-Origin Policy),考量到使用者的資訊安全。

假設 Alice 是一個惡意開發者,他撰寫的網站在運作的過程中,會嘗試透過 XHR 打向 Facebook、Instagram 等目標網站。如果使用者原先就有目標網站的登入狀態,Alice 便能窺探該使用者的隱私,取得不該取得的資訊。

再設想,如果目標網站換成 Email、銀行、電商,甚至 GitHub,若沒有瀏覽器限制跨域請求的保護,惡意開發者便能為所欲為。

跨域請求雖然會被瀏覽器擋下來,但攔截的是回應(Response),不是請求(Request)。請求指定的內容仍然會完成,開發者要特別注意這點!

解決方案#

關於跨域請求,解決方案有不少種,例如鼎鼎大名的 JSONP,也就是透過 HTML 中沒有跨域限制的標籤如 imgscript 等,再藉由指定 callback 函式,將回應內容介接回 JavaScript 中;或是透過 iframe,繞過跨域保護取得目標資源等等。

接下來會說明兩種常見、也相對正規的解決方式。

CORS#

最標準、正確的解決方法是透過 W3C 規範的「跨來源資源共用(CORS, Cross-Origin Resource Sharing)」,透過伺服器在 HTTP Header 的設定,讓瀏覽器能取得不同來源的資源。

CORS 規範中,清楚定義了跨域存取控制的運作方式。

首先,伺服器端需要在回應的 Header 加上如 Access-Control-Allow-OriginAccess-Control-Request-MethodAccess-Control-Request-Headers 等設定,限制伺服器能接受的來源、使用的方法、可攜帶的 Header 等等。

當瀏覽器發送資源請求時:

  • 如果是簡單請求(Simple Request),便會直接送出請求
  • 若不符合前述條件,則會透過**預檢請求(Preflight Request)**先敲敲門,確認可以通過伺服器限制,才會發送正式的請求

CORS 除了前述的內容外,也有關於 Cookies 的傳遞方式,如何允許跨域寫入 Cookies 等等。

代理伺服器#

由於 CORS 的 Header 設定是在伺服器端,若是自家產品,可以輕易調整伺服器設定,讓前端能取得必要的資源。但如果是介接外部 API,總不能每次遇到 CORS 錯誤,就要求外部修改 Header 設定。

一個簡單暴力的作法,是透過代理伺服器幫我們取得資源。由於跨域保護的限制是瀏覽器的規範,只要不透過瀏覽器發送請求,自然也就不會有限制。

常見的作法可以打向另外架構的中介伺服器,或是透過 nginx 做簡易的反向代理。例如在自己的開發環境,前後端分離的架構,前後端伺服器分別起在 3000 與 5000 PORT,可以透過這樣的設定:

server{
  listen 3000;
  server_name localhost;
  location ^~ /api {
  proxy_pass http://localhost:5000;
  }
}

當前端需要發送 API Request 時,可以直接打向 localhost:3000/api/...,這個請求就會被 nginx 攔截,並轉送給後端所在的 localhost:5000,這樣就能簡單的規避掉跨域保護。

結語#

跨域是實務上很常遇到的需求,CORS 的錯誤訊息也是前端初學者很容易卡住的地方。其實只要清楚 CORS 規範中的 Headers,並在伺服器做相對應的調整,就可以順利地完成跨域請求。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10226262