本文 AI 產出,尚未審核

ExpressJS (TypeScript) – CORS 與安全性設定

單元:Rate Limit 流量限制


簡介

在 Web API 的開發過程中,流量限制(Rate Limiting) 是防止服務被過度請求、保護資源、提升穩定性的重要手段。
無論是公開的公開 API、內部微服務,或是前端單頁應用(SPA)呼叫的後端,都有可能因為惡意攻擊、爬蟲或使用者操作失誤而產生大量請求。若沒有適當的限制,伺服器可能出現 CPU 飽和、記憶體泄漏,甚至整站宕機

本單元將以 ExpressJS + TypeScript 為基礎,說明如何在專案中安全、彈性地加入 Rate Limit 機制,並結合 CORS、Helmet 等安全套件,打造完整的防護層。


核心概念

1️⃣ 為什麼需要 Rate Limit?

風險 可能的影響
暴力破解 短時間內大量嘗試登入,導致帳號被盜
DoS/DDoS 伺服器資源被耗盡,正常使用者無法存取
爬蟲濫用 大量抓取資料造成資料庫負載過高
意外迴圈 前端程式錯誤導致無限請求,影響服務品質

透過 IP、使用者 ID、路由 等維度的限制,我們可以 分散風險,讓單一來源的異常流量不會影響整體系統。


2️⃣ Rate Limit 的基本原理

  1. 計數器:對每個「鍵」(Key) 保存一段時間內的請求次數。
  2. 時間窗口:如「每 15 分鐘」或「每秒」等,窗口結束後計數器重置。
  3. 超過上限的處理:回傳 429 Too Many Requests,或自訂錯誤訊息。

常見的實作方式有:

  • 內存 (MemoryStore):簡單、適合單機開發環境。
  • 分散式儲存:Redis、MongoDB、Memcached 等,用於多實例或容器化部署。

3️⃣ 在 Express + TypeScript 中導入 express-rate-limit

3.1 安裝套件

npm i express-rate-limit
npm i -D @types/express-rate-limit
# 若使用 Redis 作為儲存層
npm i rate-limit-redis ioredis

3.2 基本範例(MemoryStore)

// src/middleware/rateLimiter.ts
import rateLimit from 'express-rate-limit';
import { Request, Response, NextFunction } from 'express';

// 建立一個全域的 Rate Limiter
export const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,          // 15 分鐘
  max: 100,                         // 每個 IP 最多 100 次請求
  message: '已超過每 15 分鐘 100 次的請求上限,請稍後再試。',
  standardHeaders: true,           // 在回應 header 中加入 RateLimit-*
  legacyHeaders: false,            // 不返回 X-RateLimit-*(較舊的標頭)
});
// src/app.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { globalLimiter } from './middleware/rateLimiter';

const app = express();

app.use(cors());          // CORS 設定
app.use(helmet());        // 基本安全標頭
app.use(globalLimiter);   // 套用全域流量限制

app.get('/api/hello', (req, res) => {
  res.json({ message: 'Hello World!' });
});

export default app;

重點standardHeaders: true 讓前端可以從 RateLimit-RemainingRateLimit-Reset 等標頭取得剩餘配額,便於實作 UI 提示。


3.3 針對特定路由自訂限制

// src/middleware/loginLimiter.ts
import rateLimit from 'express-rate-limit';

export const loginLimiter = rateLimit({
  windowMs: 10 * 60 * 1000,    // 10 分鐘
  max: 5,                     // 每個 IP 最多 5 次登入嘗試
  handler: (req, res) => {
    res.status(429).json({
      error: '登入失敗次數過多,請稍後再試。',
    });
  },
});
// src/routes/auth.ts
import { Router } from 'express';
import { loginLimiter } from '../middleware/loginLimiter';

const router = Router();

router.post('/login', loginLimiter, (req, res) => {
  // 這裡放驗證邏輯
  res.json({ success: true });
});

export default router;

技巧:對於安全敏感的路由(如登入、註冊)使用更嚴格的限制,可有效降低暴力破解的成功機率。


3.4 使用 Redis 作為分散式儲存

// src/middleware/redisRateLimiter.ts
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const redisClient = new Redis({
  host: 'redis-server',   // Docker / Kubernetes 內的服務名稱
  port: 6379,
  password: process.env.REDIS_PWD,
});

export const apiLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args: string[]) => redisClient.call(...args),
  }),
  windowMs: 60 * 1000,   // 1 分鐘
  max: 60,               // 每分鐘最多 60 次請求
  keyGenerator: (req) => {
    // 若使用 JWT,則以使用者 ID 為 key;否則回退 IP
    return (req.user?.id as string) || req.ip;
  },
  skipFailedRequests: true, // 不計算 5xx 錯誤
});
// src/app.ts
import { apiLimiter } from './middleware/redisRateLimiter';

app.use('/api', apiLimiter); // 只針對 /api 前綴的路由套用

說明

  • RedisStore 讓多個 Node 實例共享同一套計數器,適合水平擴展。
  • keyGenerator 可自訂「依據」——例如使用者的 JWT sub、API 金鑰或 IP。
  • skipFailedRequests 可避免惡意攻擊者利用失敗請求耗盡配額。

3.5 結合 CORS 與 Helmet 的安全性設定

app.use(
  cors({
    origin: ['https://example.com', 'https://admin.example.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    credentials: true,
  })
);

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", 'https://trusted.cdn.com'],
        // 其他 CSP 設定...
      },
    },
  })
);

提示:在 CORS 設定中加入 credentials: true 時,務必限制 origin 為可信域名,避免 CSRF 風險。


常見陷阱與最佳實踐

陷阱 可能後果 解決方案
使用 MemoryStore 在多實例環境 計數不一致,導致限制失效 改用 Redis、MongoDB 等分散式儲存
未排除靜態資源 大量圖片、CSS 請求也被計數,影響正常流量 /static/public 設定 skipskipSuccessfulRequests
只依賴 IP NAT、VPN 會讓多位使用者共用同一 IP,產生誤判 結合 JWT、API Key 等身份資訊作為 key
回傳過於詳細的錯誤訊息 攻擊者可推測限制規則 只回傳通用訊息,避免透露 windowMsmax 數值
忽略跨域的 preflight (OPTIONS) 請求 OPTIONS 被算入限制,導致正常請求被阻斷 在 limiter 中使用 skip 判斷 req.method === 'OPTIONS'

最佳實踐清單

  1. 分層限制:全域、API 前綴、單一路由分別設定不同的上限。
  2. 使用標準 Header:讓前端 UI 能即時顯示剩餘配額,提升使用者體驗。
  3. 設定 retry-after:在 429 回應中加入 Retry-After,告訴客戶端何時可重新發送。
  4. 監控與告警:將 Redis 計數或 rate-limit 事件寫入日誌,結合 Grafana/Prometheus 監控。
  5. 測試:在開發環境使用 supertestk6 壓測,確保限制行為符合預期。

實際應用場景

場景 需求 建議的 Rate Limit 設定
公共 API (如天氣、匯率) 防止外部開發者濫用 windowMs: 1h, max: 1000, keyGenerator: API Key
登入 / 註冊 防止暴力破解 windowMs: 10m, max: 5, keyGenerator: IP
檔案上傳 API 控制資源消耗 windowMs: 5m, max: 20, skipFailedRequests: true
內部微服務間呼叫 防止服務間循環呼叫 windowMs: 30s, max: 200, keyGenerator: Service Name
即時聊天室 防止訊息刷屏 windowMs: 1s, max: 10, keyGenerator: User ID

案例:某電商平台在商品搜尋 API 加入 apiLimiter(Redis Store),每位使用者每秒最多 5 次請求。透過 keyGenerator 以 JWT sub 為鍵,成功降低了爬蟲對搜尋結果的抓取量,同時不影響正常使用者的搜尋體驗。


總結

  • Rate Limiting 是保護 Express API 的第一道防線,能有效防止 DoS、暴力破解與資源濫用。
  • 透過 express-rate-limit 搭配 RedisStore,我們可以在單機與分散式環境中彈性調整限制策略。
  • CORSHelmetRate Limiting 互補,形成完整的安全防護鏈。
  • 實務上應根據 路由敏感度使用者身份業務需求 設計分層的限制規則,並配合 監控、日誌與測試,確保系統在高流量情境下仍保持可用性與安全性。

最後提醒:安全不是一次性的設定,而是持續觀察與調整的過程。將 Rate Limit 與其他安全機制結合,才能在面對日益複雜的攻擊時,保持服務的穩定與信任。祝開發順利!