Middleware 執行順序決定一切
Express middleware 是一條線,request 從頭到尾跑過去。順序錯了,結果就錯:
- rate limiter 放在 auth middleware 後面 → 已認證的用戶也能刷爆
- cors 放在 helmet 後面 → preflight request 可能被擋掉
- body parser 放在 auth 後面 → auth middleware 讀不到 body 裡的 token
下面按照建議順序列出,說明每層在做什麼。
第一層:Infrastructure(最先跑,不依賴任何業務狀態)
helmet()
設定安全相關的 HTTP response headers(CSP、X-Frame-Options、HSTS 等)。沒有 side effect,無論如何都要裝,放最前面。
import helmet from 'helmet';
app.use(helmet());詳細說明見 API Security。
cors()
處理跨域請求的 preflight 和 headers。要在 body parser 之前,否則 OPTIONS request 被擋掉後 body parser 根本不會跑。
import cors from 'cors';
app.use(cors({
origin: allowedOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));compression()
gzip 壓縮 response body。放在 response 被送出前的最早位置,對所有 route 生效。
import compression from 'compression';
app.use(compression());純 API(JSON response)有明顯效果;已走 CDN 且 CDN 做了壓縮的情況可以省略。
morgan / Access Log
每個 request 進來就記一筆 access log。要在所有 middleware 最前面——否則如果後面某個 middleware 直接 res.end(),morgan 可能記不到。
import morgan from 'morgan';
app.use(morgan('combined')); // production 用 combined 格式
// 或接自訂 logger
app.use(morgan('combined', { stream: { write: msg => logger.info(msg.trim()) } }));Request ID(自建)
每個 request 給一個唯一 ID,貫穿整個請求生命週期。要在所有 log 之前設定,否則後面記的 log 都沒有 correlation ID。
import { v4 as uuidv4 } from 'uuid';
import { AsyncLocalStorage } from 'node:async_hooks';
export const requestContext = new AsyncLocalStorage<{ requestId: string }>();
app.use((req, res, next) => {
const requestId = (req.headers['x-request-id'] as string) || uuidv4();
res.setHeader('X-Request-Id', requestId);
requestContext.run({ requestId }, next);
});詳見 Structured Logging(correlation ID)。
第二層:Body Parsing & Size Limiting
// JSON body,限制 1mb 防 DoS
app.use(express.json({ limit: '1mb' }));
// URL-encoded form(如果有 form submit 的話)
app.use(express.urlencoded({ extended: true, limit: '1mb' }));file upload 的 route 要用 multer 單獨處理,不走這裡。
第三層:Rate Limiting(認證前)
IP-based rate limiting 必須在 auth middleware 之前:
- 目的是擋未認證的濫用(掃描、暴力破解)
- 放在 auth 後面,攻擊者連認證都不需要,可以先用 bot 刷認證 endpoint
import rateLimit from 'express-rate-limit';
// 一般 API:每 15 分鐘 100 次
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
});
// 登入 endpoint:更嚴格(防暴力破解)
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
skipSuccessfulRequests: true, // 登入成功不算在 limit 裡
});
app.use('/api', apiLimiter);
app.use('/auth', authLimiter);詳見 Rate Limiting。
第四層:Maintenance Mode(在 auth 之前擋全站)
系統維護時直接回 503,不需要進 auth 驗證:
app.use((req, res, next) => {
if (process.env.MAINTENANCE_MODE === 'true') {
// 健康檢查 endpoint 例外,讓 load balancer 知道還活著
if (req.path === '/health') return next();
return res.status(503).json({
error: 'Service Unavailable',
message: '系統維護中,請稍後再試',
retryAfter: process.env.MAINTENANCE_UNTIL,
});
}
next();
});第五層:Authentication
驗證 JWT / session,把 user 注入 req.user:
const authenticate = async (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload as AuthUser;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
// 套在需要認證的 routes 上,不是全站
router.use(authenticate);第六層:Per-Route Middleware(在 router 內)
這些是 route 層級的,不是全站的:
Permission Guard(RBAC)
const requirePermission = (permission: string) => {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user.permissions.includes(permission)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
};
router.delete('/users/:id',
requirePermission('users:delete'),
deleteUserHandler
);Ownership Check(ABAC)
詳見 ABAC 設計。
Request Validation
import { z } from 'zod';
const validate = (schema: z.ZodSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation Error',
details: result.error.flatten(),
});
}
req.body = result.data;
next();
};
};詳見 資料綁定與驗證。
第七層:Audit Log(在業務邏輯之後)
有些 audit log 需要知道操作的結果(成功/失敗),所以不能放最前面:
// 用 response interceptor 的方式,在 res.json 被呼叫時觸發
const auditLog = (action: string) => {
return (req: AuthRequest, res: Response, next: NextFunction) => {
const originalJson = res.json.bind(res);
res.json = (body) => {
if (res.statusCode < 400) {
// 只記成功的操作
logger.audit({
actor: req.user?.id,
action,
resource: req.params.id,
ip: req.ip,
});
}
return originalJson(body);
};
next();
};
};
router.delete('/users/:id',
authenticate,
requirePermission('users:delete'),
auditLog('users:delete'), // 放在 guard 之後,業務邏輯之前
deleteUserHandler
);最後層:Error Handler
Express 的 error handler 是四個參數的 middleware,放在所有 route 和 middleware 之後:
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error('Unhandled error', { error: err.message, stack: err.stack });
if (err instanceof AppError) {
return res.status(err.statusCode).json({ error: err.message, code: err.code });
}
res.status(500).json({ error: 'Internal Server Error' });
});詳見 錯誤處理機制。
順序總覽
request 進來
│
├─ helmet() → security headers
├─ cors() → cross-origin
├─ compression() → gzip
├─ morgan / access log → 流量記錄
├─ request ID → correlation ID
│
├─ body parser → 解析 body
│
├─ rate limiter → IP 限流(認證前)
├─ maintenance mode → 維護模式擋截
│
├─ authenticate → JWT 驗證
│
├─ [route-level]
│ ├─ permission guard → RBAC
│ ├─ ownership check → ABAC
│ ├─ validate → request 格式驗證
│ └─ audit log → 操作記錄
│
├─ route handler → 業務邏輯
│
└─ error handler → 統一錯誤回應
第三方套件速查
| 套件 | 用途 | 備註 |
|---|---|---|
helmet | Security headers | 必裝 |
cors | 跨域處理 | 必裝 |
morgan | HTTP request logging | 可換成自訂 winston stream |
compression | gzip 壓縮 | 純 API 效果顯著 |
express-rate-limit | IP rate limiting | Redis store 版可跨 pod |
multer | File upload | 只用在 upload route |
express-session | Session-based auth | Bearer token auth 不需要 |
i18next-http-middleware | 多語系 | 放在 body parser 後 |
response-time | 記錄 response 時間 | 加進 log 比較有用 |
