iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Modern Web

《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記系列 第 11 篇

Day 11|跨不過的牆:CORS 設定明明開了,前端卻還是拿不到回應?

  • 分享至 

  • xImage
  •  

隨著專案進入前後端串接的階段,我們的 API 終於要面對真實世界的考驗了。說到前後端分離,開發者一定都遇過一堵無形的牆——跨來源資源共用(CORS)。

你可能會想:
「這有什麼難的?在 NestJS 裡只要呼叫 app.enableCors() 不就搞定了嗎?」

今天,我們就來破解這個阻擋無數前後端順利通靈(串接)的經典魔咒!

問題怎麼發生?

在前後端串接時,前端回報請求需要攜帶 Cookie(例如 Session 或 JWT Refresh Token)。為了開發方便與省事,你隨手在 NestJS 的 main.ts 加上了這段設定:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 地雷:同時允許所有來源與 Credentials
  app.enableCors({
    origin: '*',
    credentials: true,
  });

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

原本想說 origin 給了 * 通通放行,credentials: true 也補上了,這樣設定應該就沒問題了。

結果前端一呼叫 API,瀏覽器的 Console 當場跳出一大串紅字:

Access to fetch at 'http://localhost:3000/user/me' from origin 'http://localhost:5173' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

明明設定了 CORS,後端 Log 也完全沒有跳出任何錯誤,為什麼前端卻完全拿不到回應?

根因:攜帶憑證時,Origin 不能使用 *

造成這個報錯的根本原因,在於瀏覽器的同源策略(Same-Origin Policy)與 CORS 安全規範。CORS 的核心目的,是控制某個來源(Origin)的 JavaScript 能否讀取另一個來源的回應資料(Response),藉此防止敏感資料跨網域洩漏。

想像一個情境,你正坐在電腦前,插著讀卡機與金融卡,準備在網銀官網修改密碼。而你的瀏覽器其實同時開著另一個分頁——你剛剛隨手點開的心理測驗網站(惡意網站)。

如果這時網銀後端 API 的設定是這樣的:

  • credentials: true:網銀系統確認了讀卡機裡插著你的實體卡片(Cookie/憑證),允許執行需要驗證身分的敏感操作。
  • origin: '*':網銀系統對請求來源完全不設防,接受來自任何網頁的請求,並允許對方獲取處理後的敏感資料。

這就是引發災難的破口!那個看似有趣無害的「心理測驗網站」背後其實藏有惡意腳本。它會趁著你的卡片還插在讀卡機裡(身分驗證處於有效狀態),偷偷在背景向網銀系統發送指令。如果沒有安全機制的保護,你的錢就在一邊測「你是哪種小動物」的同時,一邊被背景的心理測驗網站給偷偷轉走了。

為了不讓這種悲劇發生,身為保全的「瀏覽器」會立刻站出來擋在前面,對後端發出嚴厲警告:
「等一下!你既然啟用了金融卡驗證(credentials: true),就必須指定只有官方網銀才能存取!你設定成不限來源(origin: *),不就等於讓惡意網站拿著用戶的憑證自由進出?這不但會看光敏感資料,要是系統沒做好 CSRF 防護,連錢都會被直接偷轉走!」

https://ithelp.ithome.com.tw/upload/images/20260925/20184306F5QyixgGu2.png

因此,為了杜絕這種資安災難,W3C 標準直接在瀏覽器層級下達了死命令:
「只要你想攜帶私密憑證(Credentials),存取回應的來源(Origin)就絕對不能是萬用字元 *!」

也就是說,就算後端順利處理完請求也回傳了資料,只要 CORS 檢查沒過,瀏覽器就會直接把回應死死扣住攔下,絕對不讓前端讀取。

常見誤解:origin: '*' 和 origin: true 一樣嗎?

有些敏銳的開發者在查閱 NestJS 的底層文件時,會發現 origin 除了填寫字串,還可以填寫布林值。

這兩者有什麼本質上的區別?

// 1. 萬用字元
app.enableCors({ origin: '*' });

// 2. 動態反映
app.enableCors({ origin: true });
origin 實際回傳的 Access-Control-Allow-Origin 標頭值 支援 credentials: true 嗎? 安全性與適用場景
'*' (Wildcard) 永遠固定回傳 * 不支援 (瀏覽器會報錯) 適合完全公開、不涉及用戶隱私的公共 API。
true (Dynamic) 自動複製請求來源的網域(req.header('Origin')) 支援 適合開發環境,但不建議直接用於生產環境,因為這等同於對全部的網域開門,容易造成資料洩漏。

⚠️ 注意:
設定 origin: true 雖然能讓瀏覽器正常運作,但這只是「將大門鑰匙掛在門口」的偷懶做法,在正式環境中依然存在極大的安全隱患。

排雷指南

解法一:嚴格實名制——明確指定允許的來源網域

最直接的方式,就是明確列舉哪些網域是可以攜帶憑證的信任白名單。

在 main.ts 中,請把 * 換成前端真實的所在網址(包含通訊協定與 Port 號):

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableCors({
    origin: ['http://localhost:5173'], // 明確指定允許的來源
    credentials: true,
    methods: ['GET', 'POST', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
  });

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

解法二:動態 Origin 搭配環境變數——彈性應對多環境與動態網域

解決了基本設定後,在實務開發與專案推進時,我們通常會面臨更複雜的架構挑戰:

  1. 多網域與多環境:同一個後端要服務 admin.my-app.com 與 www.my-app.com,或是需區分本機、測試、正式環境。如果寫死在程式碼裡,每次部署都要修改程式碼,容易出錯。
  2. 避免靜態陣列維護困難:每次前端新增活動頁面或測試網域,後端都要跟著修改程式並重啟服務,違反了前後端分離的初衷。

進階做法:使用函式形式的 origin 結合環境變數

NestJS 的 origin 設定非常強大,它除了接受字串與陣列,更接受一個 函式(Function)!搭配環境變數,我們可以做出極具彈性且兼顧安全的防護網:

// 從環境變數讀取逗號分隔的白名單字串,並轉為陣列
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];

app.enableCors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
      return;
    }

    callback(null, false);
  },
  credentials: true,
});

現在,你只需要在正式環境的 .env 檔案中配置:

# .env.production
ALLOWED_ORIGINS=https://www.my-app.com,https://admin.my-app.com

而在本地開發環境的 .env 配置:

# .env.development
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000

之後無論要切換環境還是新增網域,都只要改改環境變數就好。

總結

  1. 憑證模式下禁用萬用字元:當前端需要跨網域傳送 Cookie 或身分憑證(credentials: true)時,origin 絕對不能設為萬用字元 *,否則會違反規範而被瀏覽器直接攔截回應。
  2. 小心動態反映的潛在風險:設定 origin: true 雖能自動反射請求來源,但在生產環境中等同於無差別信任所有網域,極易造成敏感資料洩漏的風險。
  3. 嚴格白名單:最安全的做法是明確設定允許的前端網域白名單。
  4. 動態驗證為最佳實踐:善用函式(Function)形式的 origin 搭配環境變數,能彈性支援多環境部署與多子網域,是兼顧安全性與維護性的最佳實踐。

參考資源


上一篇
Day 10|消失的路由:為什麼靜態路徑總是被動態參數給攔截?
下一篇
Day 12|失靈的守衛:為什麼 DTO 不能用 interface 和 type?
系列文
《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言