本文 AI 產出,尚未審核

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)請求:瀏覽器在發送帶有自訂標頭或非簡單方法(如 PUTDELETE)的請求前,會先發送 OPTIONS 預檢請求。cors 會自動回應正確的 Access-Control-* 標頭,省去手寫邏輯的麻煩。
  • 型別安全:配合 TypeScript,cors 的設定物件有完整的介面 (CorsOptions) 可供編譯期檢查,減少因拼寫錯誤導致的執行時錯誤。
  • 彈性配置:支援全域、路由層級、動態判斷來源等多種使用情境。

2. CORS 基本流程

  1. 瀏覽器發送請求(若是簡單請求直接發送,若是非簡單請求先發送 OPTIONS)。
  2. 伺服器回應Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 等標頭。
  3. 瀏覽器根據回應判斷是否允許,若允許則把真正的請求結果返回給前端。

⚠️ 若伺服器未回傳正確的 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: trueAccess-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,瀏覽器仍不會送出。 前端 fetchaxios 必須明確設定。
Preflight 請求被其他中介軟體(如 helmet)阻擋 OPTIONS 請求回傳 404 或 500,導致所有非簡單請求失敗。 確保 cors 於所有其他中介軟體之前掛載,或在 helmet 設定中允許 crossOriginResourcePolicy.
在生產環境忘記移除開發用的 origin: '*' 任何網站都能呼叫你的 API,可能造成資料外洩或濫用。 使用環境變數切換白名單,並在 CI/CD 中做檢查。
錯誤處理未捕獲 cors 的 callback error 當來源不在白名單時,伺服器直接回 500,前端得到不明錯誤。 origin callback 中回傳 new Error(),並在全局錯誤中統一處理。

最佳實踐

  1. 最小化允許來源:僅允許已知的前端 URL,絕不要在生產環境使用 *
  2. 分層管理:全域設定僅處理最基本的 CORS,敏感路由使用更嚴格的白名單或動態檢查。
  3. 配合 helmet:在 helmet 中設定 crossOriginResourcePolicy: { policy: "same-site" },加強資源保護。
  4. 使用 TypeScript 型別:透過 CorsOptions 讓設定在編譯階段即被檢查,避免拼寫錯誤。
  5. 記錄與監控:在 origin callback 中加入日誌,追蹤被拒絕的來源,有助於安全審計。

實際應用場景

  1. 多前端子域名(SPA、管理後台)共用同一 API

    • 透過白名單或動態 origin,讓 https://app.mycompany.comhttps://admin.mycompany.com 同時存取。
  2. 行動 App 使用 WebView 呼叫同一套 API

    • 行動端的 WebView 會有 file:// 或自訂 scheme,需在 origin 中加入 null 或自訂檢查。
  3. 第三方合作夥伴嵌入 widget

    • 為特定合作夥伴的子域名開放 CORS,其他來源則拒絕,確保資料不會被任意站點抓取。
  4. 微服務間內部呼叫

    • 內部服務通常不需要 CORS,僅在 API Gateway 暴露給前端時才加入 cors 中介軟體,減少不必要的開銷。
  5. 跨域上傳檔案(使用 multipart/form-data

    • 因為檔案上傳屬於非簡單請求,需要正確回應 Access-Control-Allow-Headers: Content-Type, Authorization,並允許 PUT/POST 方法。

總結

  • CORS 是前端與跨域 API 互動的門檻,正確設定不僅能讓功能正常運作,更是保護資源不被任意網站濫用的第一道防線。
  • cors 套件與 TypeScript 的結合,提供了 型別安全、彈性配置 以及 自動處理 preflight 的便利。
  • 全域開放白名單、動態判斷、路由層級 的多層次設定,你可以依照不同的業務需求,靈活調整策略。
  • 請務必避免 origin: '*' + credentials:true 的錯誤組合,並在正式環境收斂來源加上日誌結合 helmet 以提升整體安全性。

掌握了上述概念與範例後,你就能在 Express + TypeScript 專案中自信地部署安全、可靠的跨域 API,為前端開發者提供順暢的使用體驗,同時保護你的後端資源不受未授權的存取。祝開發順利!