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         → 統一錯誤回應

第三方套件速查

套件用途備註
helmetSecurity headers必裝
cors跨域處理必裝
morganHTTP request logging可換成自訂 winston stream
compressiongzip 壓縮純 API 效果顯著
express-rate-limitIP rate limitingRedis store 版可跨 pod
multerFile upload只用在 upload route
express-sessionSession-based authBearer token auth 不需要
i18next-http-middleware多語系放在 body parser 後
response-time記錄 response 時間加進 log 比較有用

延伸閱讀