本文 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 的基本原理
- 計數器:對每個「鍵」(Key) 保存一段時間內的請求次數。
- 時間窗口:如「每 15 分鐘」或「每秒」等,窗口結束後計數器重置。
- 超過上限的處理:回傳
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-Remaining、RateLimit-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可自訂「依據」——例如使用者的 JWTsub、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 設定 skip 或 skipSuccessfulRequests |
| 只依賴 IP | NAT、VPN 會讓多位使用者共用同一 IP,產生誤判 | 結合 JWT、API Key 等身份資訊作為 key |
| 回傳過於詳細的錯誤訊息 | 攻擊者可推測限制規則 | 只回傳通用訊息,避免透露 windowMs、max 數值 |
| 忽略跨域的 preflight (OPTIONS) 請求 | OPTIONS 被算入限制,導致正常請求被阻斷 | 在 limiter 中使用 skip 判斷 req.method === 'OPTIONS' |
最佳實踐清單
- 分層限制:全域、API 前綴、單一路由分別設定不同的上限。
- 使用標準 Header:讓前端 UI 能即時顯示剩餘配額,提升使用者體驗。
- 設定
retry-after:在 429 回應中加入Retry-After,告訴客戶端何時可重新發送。 - 監控與告警:將 Redis 計數或
rate-limit事件寫入日誌,結合 Grafana/Prometheus 監控。 - 測試:在開發環境使用
supertest或k6壓測,確保限制行為符合預期。
實際應用場景
| 場景 | 需求 | 建議的 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以 JWTsub為鍵,成功降低了爬蟲對搜尋結果的抓取量,同時不影響正常使用者的搜尋體驗。
總結
- Rate Limiting 是保護 Express API 的第一道防線,能有效防止 DoS、暴力破解與資源濫用。
- 透過 express-rate-limit 搭配 RedisStore,我們可以在單機與分散式環境中彈性調整限制策略。
- CORS、Helmet 與 Rate Limiting 互補,形成完整的安全防護鏈。
- 實務上應根據 路由敏感度、使用者身份 與 業務需求 設計分層的限制規則,並配合 監控、日誌與測試,確保系統在高流量情境下仍保持可用性與安全性。
最後提醒:安全不是一次性的設定,而是持續觀察與調整的過程。將 Rate Limit 與其他安全機制結合,才能在面對日益複雜的攻擊時,保持服務的穩定與信任。祝開發順利!