ExpressJS (TypeScript) – CORS 與安全性設定
使用 cors 套件
簡介
在前後端分離的現代 Web 應用中,跨來源資源共享(CORS) 是必不可少的機制。瀏覽器會根據同源政策限制前端直接呼叫不同來源的 API,若未正確設定 CORS,前端將無法取得資料,甚至會出現「No ‘Access‑Control‑Allow‑Origin’ header is present」的錯誤訊息。
Express 是 Node.js 最常用的 Web 框架,而 cors 套件則提供了一套簡潔且可配置的方式,讓開發者在 TypeScript 專案中快速完成安全的跨域設定。本文將從核心概念說明到實作範例,逐步帶你建立既安全又彈性的 CORS 策略。
核心概念
1. 為什麼要使用 cors 套件?
- 自動處理 preflight(OPTIONS)請求:瀏覽器在發送帶有自訂標頭或非簡單方法(如
PUT、DELETE)的請求前,會先發送OPTIONS預檢請求。cors會自動回應正確的Access-Control-*標頭,省去手寫邏輯的麻煩。 - 型別安全:配合 TypeScript,
cors的設定物件有完整的介面 (CorsOptions) 可供編譯期檢查,減少因拼寫錯誤導致的執行時錯誤。 - 彈性配置:支援全域、路由層級、動態判斷來源等多種使用情境。
2. CORS 基本流程
- 瀏覽器發送請求(若是簡單請求直接發送,若是非簡單請求先發送
OPTIONS)。 - 伺服器回應
Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers等標頭。 - 瀏覽器根據回應判斷是否允許,若允許則把真正的請求結果返回給前端。
⚠️ 若伺服器未回傳正確的 CORS 標頭,瀏覽器會在 network 面板顯示 CORS 錯誤,且前端的
fetch/axios會拋出 NetworkError。
3. cors 套件的核心型別
import type { CorsOptions } from 'cors';
const options: CorsOptions = {
origin: 'https://example.com', // 允許的來源
methods: ['GET', 'POST'], // 允許的方法
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // 是否允許帶 cookie
preflightContinue: false, // 預檢請求不交給後續中介軟體
optionsSuccessStatus: 204, // 預檢成功的回應碼
};
程式碼範例
以下示範 5 個常見且實用的 CORS 設定,全部使用 TypeScript 撰寫,並加上說明註解。
1️⃣ 基本全域設定(允許所有來源)
// src/app.ts
import express from 'express';
import cors from 'cors';
const app = express();
// 允許任何來源的請求(開發階段常用,正式環境建議收斂)
app.use(cors());
app.get('/api/hello', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
💡 小技巧:開發環境若使用
npm run dev,可在dotenv中設定CORS_ORIGIN=*,讓程式碼更具彈性。
2️⃣ 白名單(Whitelist)方式限定來源
// src/middleware/corsWhitelist.ts
import { Request, Response, NextFunction } from 'express';
import cors, { CorsOptions } from 'cors';
const whitelist = ['https://myfrontend.com', 'https://admin.myapp.com'];
const corsOptions: CorsOptions = {
origin: (origin, callback) => {
// 若無來源(如 Postman)則直接允許
if (!origin) return callback(null, true);
if (whitelist.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true, // 允許 cookie
};
export const corsWhitelist = cors(corsOptions);
// src/app.ts
import express from 'express';
import { corsWhitelist } from './middleware/corsWhitelist';
const app = express();
// 只在需要的路由套用白名單策略
app.use('/api', corsWhitelist);
app.get('/api/secure-data', (req, res) => {
res.json({ secret: '🔐 only whitelisted origins can see me' });
});
app.listen(3000);
3️⃣ 允許帶 Cookie(credentials: true)的設定
// src/app.ts
import express from 'express';
import cors from 'cors';
const app = express();
app.use(
cors({
origin: 'https://myfrontend.com',
credentials: true, // 必須與前端的 fetch({ credentials: 'include' }) 同步
})
);
// 設定 Session(示範用)
import session from 'express-session';
app.use(
session({
secret: 'super-secret',
resave: false,
saveUninitialized: false,
cookie: { secure: false, httpOnly: true },
})
);
app.get('/api/profile', (req, res) => {
// 假設已登入,Session 中有 user
res.json({ user: req.session?.user ?? null });
});
app.listen(3000);
⚠️ 注意:若
credentials: true,Access-Control-Allow-Origin不能 使用*,必須是具體的來源網址。
4️⃣ 路由層級的 CORS 設定(不同路由不同策略)
// src/app.ts
import express from 'express';
import cors from 'cors';
const app = express();
// 公開 API:允許所有來源
app.use('/public', cors(), express.Router().get('/', (req, res) => {
res.json({ data: 'public data' });
}));
// 私有 API:僅限特定來源,且允許 cookie
const privateCors = cors({
origin: 'https://admin.myapp.com',
credentials: true,
});
app.use('/admin', privateCors, express.Router().get('/dashboard', (req, res) => {
res.json({ admin: 'dashboard data' });
}));
app.listen(3000);
5️⃣ 動態產生 origin(根據環境變數或請求 Header)
// src/middleware/dynamicCors.ts
import cors, { CorsOptions } from 'cors';
import { Request } from 'express';
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') ?? [];
const dynamicOptions: CorsOptions = {
origin: (origin: string | undefined, callback) => {
// 若是同源請求或沒有 origin(Server-to-Server),直接允許
if (!origin) return callback(null, true);
// 允許清單內的任意來源
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
// 其他來源拒絕
return callback(new Error('Origin not allowed by CORS'));
},
credentials: true,
};
export const dynamicCors = cors(dynamicOptions);
// src/app.ts
import express from 'express';
import { dynamicCors } from './middleware/dynamicCors';
const app = express();
// 只在需要的路由套用
app.use('/api', dynamicCors);
app.get('/api/data', (req, res) => {
res.json({ info: 'dynamic CORS based on env' });
});
app.listen(3000);
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 建議的解決方案 |
|---|---|---|
使用 origin: '*' 同時開啟 credentials:true |
瀏覽器會拋出 CORS policy 錯誤,因為 * 無法與 credentials 共存。 |
指定具體來源或使用白名單機制。 |
忘記在前端加上 credentials: 'include' |
即使伺服器允許 cookie,瀏覽器仍不會送出。 | 前端 fetch 或 axios 必須明確設定。 |
Preflight 請求被其他中介軟體(如 helmet)阻擋 |
OPTIONS 請求回傳 404 或 500,導致所有非簡單請求失敗。 | 確保 cors 於所有其他中介軟體之前掛載,或在 helmet 設定中允許 crossOriginResourcePolicy. |
在生產環境忘記移除開發用的 origin: '*' |
任何網站都能呼叫你的 API,可能造成資料外洩或濫用。 | 使用環境變數切換白名單,並在 CI/CD 中做檢查。 |
錯誤處理未捕獲 cors 的 callback error |
當來源不在白名單時,伺服器直接回 500,前端得到不明錯誤。 | 在 origin callback 中回傳 new Error(),並在全局錯誤中統一處理。 |
最佳實踐:
- 最小化允許來源:僅允許已知的前端 URL,絕不要在生產環境使用
*。 - 分層管理:全域設定僅處理最基本的 CORS,敏感路由使用更嚴格的白名單或動態檢查。
- 配合
helmet:在helmet中設定crossOriginResourcePolicy: { policy: "same-site" },加強資源保護。 - 使用 TypeScript 型別:透過
CorsOptions讓設定在編譯階段即被檢查,避免拼寫錯誤。 - 記錄與監控:在
origincallback 中加入日誌,追蹤被拒絕的來源,有助於安全審計。
實際應用場景
多前端子域名(SPA、管理後台)共用同一 API
- 透過白名單或動態
origin,讓https://app.mycompany.com、https://admin.mycompany.com同時存取。
- 透過白名單或動態
行動 App 使用 WebView 呼叫同一套 API
- 行動端的 WebView 會有
file://或自訂 scheme,需在origin中加入null或自訂檢查。
- 行動端的 WebView 會有
第三方合作夥伴嵌入 widget
- 為特定合作夥伴的子域名開放 CORS,其他來源則拒絕,確保資料不會被任意站點抓取。
微服務間內部呼叫
- 內部服務通常不需要 CORS,僅在 API Gateway 暴露給前端時才加入
cors中介軟體,減少不必要的開銷。
- 內部服務通常不需要 CORS,僅在 API Gateway 暴露給前端時才加入
跨域上傳檔案(使用
multipart/form-data)- 因為檔案上傳屬於非簡單請求,需要正確回應
Access-Control-Allow-Headers: Content-Type, Authorization,並允許PUT/POST方法。
- 因為檔案上傳屬於非簡單請求,需要正確回應
總結
- CORS 是前端與跨域 API 互動的門檻,正確設定不僅能讓功能正常運作,更是保護資源不被任意網站濫用的第一道防線。
cors套件與 TypeScript 的結合,提供了 型別安全、彈性配置 以及 自動處理 preflight 的便利。- 從 全域開放 到 白名單、動態判斷、路由層級 的多層次設定,你可以依照不同的業務需求,靈活調整策略。
- 請務必避免
origin: '*'+credentials:true的錯誤組合,並在正式環境收斂來源、加上日誌、結合helmet以提升整體安全性。
掌握了上述概念與範例後,你就能在 Express + TypeScript 專案中自信地部署安全、可靠的跨域 API,為前端開發者提供順暢的使用體驗,同時保護你的後端資源不受未授權的存取。祝開發順利!