前端如何處理 CORS

Lastor
Code 隨筆放置場
11 min readSep 24, 2022

幹前端或多或少都撞過 CORS 問題,雖然未必都會碰上,但也是早晚的問題。

這邊附上之前自己針對 CORS 而設置的代理工具,相關作法下面會提到。

何謂 CORS

在打 API 時候,如果碰到瀏覽器跳出這類錯誤,那就是撞上 CORS 問題了。

Access to XMLHttpRequest at 'http://localhost:3000/' from origin 'http://localhost:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

這問題是出在瀏覽器的同源政策 (same-origin policy) 身上,基於安全性考量,不允許跨來源資源共用 (cross-origin-resource-sharing),也就是不能對不同的 host 請求資源,簡稱為 CORS。

上面的錯誤訊息也寫明了,我從 localhost:5500 對 localhost:3000 發送 request,但由於 Server 回應的 origin header 並沒有被設定,也就是 Server 回傳的 origin 跟發請求的 origin 不一樣,所以瀏覽器將這個 response 擋了下來。

這個可以自己簡單起一個 Express.js Server,啥都不設定,隨便回傳點東西,然後用 VSCode 的 Live Server 之類,起一個前端 Server,然後在瀏覽器上去打打看 API,應該就會跳出 CORS 了。

// Express Server on port 3000
app.use((req, res) => {
res.json({ status: 'success', message: 'ok' })
})
// Client Browser on port 5500
;(async () => {
const { data } = await axios.get('http://localhost:3000')
})()

後端開放跨域請求

要解決這問題的正規做法,是要在 Server 端設定 Headers,讓返回的 response Header 與發送的 origin 相同,瀏覽器就會判斷是同源,也就不會跑出 CORS 錯誤了。一般開發時會偷懶,直接設定「*」表示任何來源都接受。

app.use((req, res) => {
// 允許所有的來源
res.append('Access-Control-Allow-Origin', '*')
res.json({ status: 'success', message: 'ok' })
})

但如果要夾帶 cookie 的登入憑證,就不能設定 * 了,瀏覽器會要求一定要明確指定 origin,而且一定要長得跟發請求的 origin 一樣才可以。

// Express Server on port 3000
app.use((req, res) => {
// 明確指定 origin
res.append('Access-Control-Allow-Origin', 'http://localhost:5500')
// 允許夾帶憑證
res.append('Access-Control-Allow-Credentials', true)
res.json({ status: 'success', message: 'ok' })
})
// Client Browser on port 5500
;(async () => {
const { data } = await axios.get('http://localhost:3000', {
withCredentials: true,
})
console.log(data)
})()

正常來說,如果產品的 Server 有不同的 host,無論正式環境或是測試機,後端都會針對自家的 domain 開 CORS 的。所以不用擔心前端上 Server 之後會撞 CORS。

但是後端通常不會幫 localhost 開 CORS,所以前端在 locahost 開發時,或多或少都會被擋 CORS。然而,其實前端也是有些對策能解決,接下來就來介紹一下前端常用的解法。

從瀏覽器端解決 CORS

這個做法比較無腦,又很簡單。就是 Chrome 之類的擴充商店,其實是有人做這方面的插件的,直接在擴充商店搜尋 CORS 就有了。

例如這個 Allow CORS 就蠻好用的,他的官網也有說明原理,只是我沒去研究就是。

下載之後只要把它打開,就能突破 CORS 限制了。有些套件也有提供進階設定供開發者使用。但開發完之後要記得關起來,不然之後我們瀏覽任何網頁,也會是突破 CORS 的狀態。

這樣的解法其實是比較方便的作法,因為前端不用做任何事,也不用改 code 就能完成。

使用反向代理解決 CORS

反向代理是一種類似 VPN 的思路,既然我們直接連線連不上,那就找一個連得上的 Server 轉發就好。由於同源政策是瀏覽器上的限制,所以後端對後端打 API 是不會被 CORS 限制住的。

也就是說,我們可以自己在本機起一個轉發用的 Proxy Server,然後 Client 端把請求改發到這個 Proxy Server 讓他幫我們轉發請求,再吐 data 回來就可以了。

// Request
Client -> Proxy Server -> Real Server
// Response
Client <- Proxy Server <- Real Server

所以前端就可以自己起一個 Express Server 來做這件事。

// Express Server
const cors = require('cors')
app.use(cors())
app.use('/api', (req, res) => {
const { data } = await axios.get('https://real-server/api/xxx')
res.json(data)
})

但這樣徒手做其實頗麻煩的,因為 api 不會只有一支,可能還會分正式機,測試機等等一大堆,要一個一個去客製很麻煩,要花時間去設定一個自動化的機制更麻煩。

所以比較複雜的情況,還是直接拉套件吧。

使用 http-proxy-middleware

http-proxy-middleware 是 Express.js 體系的中間件,可以幫我們簡化設定代理伺服器的力氣。

但其實第一次用的時候會有點矇就是,個人當初也是花了點時間才搞懂怎麼用。

例如說,各 Server 的位置如下:
實際 API path => http://localhost:8080/api/hello
Proxy Server => http://localhost:3000

然後我要在前端改對 Proxy Server 打 API,透過代理幫我轉發請求以獲取資料。首先在 Proxy 機上面引入並設定 http-proxy-middle。

// 後端 Proxy Server 3000
const { proxy } = require('http-proxy-middleware')
/** @type {import('http-proxy-middleware/dist/types').Options} */
const option = {
target: 'http://localhost:8080', // 實際目標 host
changeOrigin: true, // 開 true 才能拿到 query string
logLevel: 'debug', // 使其顯示基本 log
onProxyReq() {}, // onRequest hook
onProxyRes() {}, // onResponse hook
}
app.use('/api', proxy(option))

設置時,如果沒有使用 Typescript,也可以利用 JSDoc 取得套件的 interface 來獲取智能提示。最基本的設定大概就只有這些,要注意的是 target 只設定協定跟 host 就好,而 path 則是將根部設定在 Express 上。

這邊實際發送路徑是 /api/hello 所以只設根部的 /api 就好,後續的路徑套件會幫我們根據前端請求的路徑自動補上。

// 前端
axios.get('http://localhost:3000/api/hello', {
withCredentials: true,
})

這樣前端對 Proxy 發出請求,由於 path 開頭是 /api 就會被 Express 捕獲, 轉交給 http-proxy-middleware 處理。然後這套件就會以這個 /api 路徑作為 base 幫我們前後拼接成 http://localhost:8080/api/hello 發送出去。

前端就可以自由的發送開頭是 /api 請求,不需要我們一個一個都去設定。

最後,要記得幫我們的 Proxy Server 也開放 CORS,不然一樣會被擋瀏覽器 CORS。使用該套件的情況,前面再用 Express 的 cors 中間件的話會有問題,所以得辛苦一點手動設置。

// 在 on response 的 hook 上設定 Headers
onProxyRes(proxyRes, req, res) {
// 允許 CORS
const origin = req.headers.origin
proxyRes.headers['Access-Control-Allow-Origin'] = origin
proxyRes.headers['Access-Control-Allow-Credentials'] = true
}

使用 Vite 設置反向代理

自己起一個 Express Server 終究還是麻煩了些,跨 CORS 反向代理算是前端很常見的需求,自然 Vite 這類打包工具也有幫我們包好。這樣就可以利用 Vite dev 啟動時建立的 Server 來充當 Proxy Server。

Vite 使用的反向代理套件是 node-http-proxy,而前面提到的 http-proxy-middleware 其實底層也是它。所以設置方式是一樣的,只是 Vite 幫我們處理了更多東西。

只要在 vite.config.js 上加個類似的設定就可以了。

// vite.config.js
import { defineConfig, ProxyOptions } from 'vite';
export default defineConfig({
server: {
proxy: {
// 要捕獲的根路由
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
}
}
})

接著,前端打 API 的目標 host 改為自己就可以了。

const isDev = import.meta.env.DEV
const API_HOST = isDev ? '' : 'https://real-api-server'
const { data } = axios.get(`${API_HOST}/api/hello`)

vite 還提供了一個 configure 屬性,可以讓我們訪問原套件的 instance。我們就可以透過它來加上一些 log 機制,來幫我們確認轉發的目標對不對。

// vite.config.js
'api': {
/** @type {import('vite').ProxyOptions['configure']} */
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
const { method, protocol, host, path } = proxyReq
console.log(`[proxy] ${method} ${protocol}//${host}${path}`)
})
},
}
// 每當轉發時, 會 log 出這樣的訊息
[proxy] GET http://localhost:8080/api/hello

更多設定可以參考 vite config 裡面的 Server Options

綜觀以上,最無腦的做法還是直接用瀏覽器的擴充套件比較快,其次就是透過 Vite 幫我們掛代理。理論上 Webpack 應該也有類似的機制,有興趣的可以再去查查看喔。

--

--

Lastor
Code 隨筆放置場

Web Frontend / 3D Modeling / Game and Animation. 設計本科生,前遊戲業 3D Artist,專擅日本動畫與遊戲相關領域。現在轉職為前端工程師,以專業遊戲美術的角度涉足 Web 前端開發。