服务端中间件与请求管道

API Routes 处理的是具体的业务逻辑,而服务端中间件处理的是横切关注点——日志记录、身份验证、CORS、限流。每个进入 Nitro 的请求都会依次经过所有中间件,再到达具体的 handler。理解中间件的执行模型,是构建安全、可观测后端的基础。

1. server/middleware/ 执行模型

1.1 基本用法

server/middleware/ 目录下的文件会在每个请求到达 handler 之前执行:

// server/middleware/logger.ts
export default defineEventHandler((event) => {
  console.log(`[${new Date().toISOString()}] ${event.method} ${event.path}`)
  // 不返回值 → 请求继续传递到下一个中间件或 handler
  // 返回值 → 请求被拦截,直接响应
})

1.2 执行顺序

中间件按文件名字母序执行。推荐用数字前缀控制顺序:

server/middleware/
├── 01.logger.ts       ← 最先执行:记录日志
├── 02.cors.ts         ← 其次:处理跨域
├── 03.auth.ts         ← 然后:身份验证
└── 04.rate-limit.ts   ← 最后:限流检查

1.3 拦截 vs 放行

  • 放行:不返回任何值(returnreturn undefined),请求继续传递
  • 拦截:返回值或抛出错误,请求到此终止
// 拦截示例:未认证时直接返回 401
export default defineEventHandler((event) => {
  if (event.path.startsWith('/api/admin') && !event.context.user) {
    throw createError({ statusCode: 401, message: '请先登录' })
  }
  // 其他路径放行
})

1.4 event.context 传递数据

中间件之间通过 event.context 共享数据(类似 Express 的 req.user):

// 中间件中设置
event.context.user = { id: 1, name: '张三', role: 'creator' }
 
// handler 中读取
const user = event.context.user

2. JWT Token 验证中间件

这是最常见的中间件场景——从请求头或 Cookie 中提取 Token,验证后将用户信息注入 event.context

// server/middleware/03.auth.ts
import jwt from 'jsonwebtoken'
 
const PUBLIC_PATHS = ['/api/auth/login', '/api/auth/register', '/api/videos']
 
export default defineEventHandler((event) => {
  // 公开接口不需要认证
  if (PUBLIC_PATHS.some(p => event.path.startsWith(p) && event.method === 'GET')) return
 
  // 从 Authorization header 或 Cookie 中提取 Token
  const authHeader = getRequestHeader(event, 'authorization')
  const token = authHeader?.replace('Bearer ', '') || parseCookies(event).token
 
  if (!token) {
    // 非 API 请求放行(页面渲染由客户端处理)
    if (!event.path.startsWith('/api/')) return
    throw createError({ statusCode: 401, message: '未提供认证凭证' })
  }
 
  try {
    const config = useRuntimeConfig()
    const payload = jwt.verify(token, config.jwtSecret) as { userId: string; role: string }
    event.context.user = payload
  } catch {
    if (!event.path.startsWith('/api/')) return
    throw createError({ statusCode: 401, message: 'Token 无效或已过期' })
  }
})

配合一个 server/utils/ 工具函数,让 handler 中的鉴权更简洁:

// server/utils/auth.ts(自动导入)
export async function requireAuth(event: H3Event) {
  const user = event.context.user
  if (!user) throw createError({ statusCode: 401, message: '请先登录' })
  return user
}
 
export async function requireRole(event: H3Event, role: string) {
  const user = await requireAuth(event)
  if (user.role !== role && user.role !== 'admin') {
    throw createError({ statusCode: 403, message: '权限不足' })
  }
  return user
}

3. 请求日志中间件

生产环境需要结构化日志,记录请求耗时和状态码:

// server/middleware/01.logger.ts
export default defineEventHandler((event) => {
  const start = Date.now()
 
  // 请求结束后记录(利用 H3 的 afterResponse hook)
  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    const status = event.node.res.statusCode
    const log = `${event.method} ${event.path} → ${status} (${duration}ms)`
 
    if (status >= 500) console.error(log)
    else if (status >= 400) console.warn(log)
    else console.log(log)
  })
})

如果需要更专业的日志,可以集成 pinoconsola,并输出 JSON 格式方便日志平台采集。

4. CORS 跨域配置

Nitro 内置了 CORS 支持,通过 nuxt.config.ts 配置即可:

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/api/**': {
        cors: true,
        headers: {
          'Access-Control-Allow-Origin': 'https://yourdomain.com',
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type, Authorization',
          'Access-Control-Max-Age': '86400',
        },
      },
    },
  },
})

如果需要更精细的控制(动态 Origin、凭证等),可以写中间件:

// server/middleware/02.cors.ts
const ALLOWED_ORIGINS = ['https://yourdomain.com', 'https://admin.yourdomain.com']
 
export default defineEventHandler((event) => {
  const origin = getRequestHeader(event, 'origin')
  if (origin && ALLOWED_ORIGINS.includes(origin)) {
    setResponseHeaders(event, {
      'Access-Control-Allow-Origin': origin,
      'Access-Control-Allow-Credentials': 'true',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    })
  }
 
  // 预检请求直接返回
  if (event.method === 'OPTIONS') {
    setResponseStatus(event, 204)
    return ''
  }
})

5. 请求限流 Rate Limiting

防止接口被滥用,基于 IP 或用户 ID 限流:

// server/middleware/04.rate-limit.ts
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
 
const WINDOW_MS = 60 * 1000   // 1 分钟窗口
const MAX_REQUESTS = 60        // 每窗口最多 60 次
 
export default defineEventHandler((event) => {
  // 只对 API 限流
  if (!event.path.startsWith('/api/')) return
 
  const ip = getRequestIP(event, { xForwardedFor: true }) || 'unknown'
  const now = Date.now()
  const record = rateLimitMap.get(ip)
 
  if (!record || now > record.resetTime) {
    rateLimitMap.set(ip, { count: 1, resetTime: now + WINDOW_MS })
    return
  }
 
  record.count++
  if (record.count > MAX_REQUESTS) {
    setResponseHeader(event, 'Retry-After', String(Math.ceil((record.resetTime - now) / 1000)))
    throw createError({ statusCode: 429, message: '请求过于频繁,请稍后再试' })
  }
})

生产环境建议:上述内存存储仅适用于单实例。多实例部署时应使用 Redis 存储限流计数,或接入 Cloudflare Rate Limiting、nginx limit_req 等基础设施级方案。

5.1 分级限流

不同接口应有不同的限流策略:

// server/utils/rate-limit.ts
const RATE_LIMITS: Record<string, { windowMs: number; max: number }> = {
  '/api/auth/login': { windowMs: 60_000, max: 5 },       // 登录:1 分钟 5 次
  '/api/auth/register': { windowMs: 3600_000, max: 3 },   // 注册:1 小时 3 次
  '/api/upload': { windowMs: 60_000, max: 10 },            // 上传:1 分钟 10 次
  '/api/': { windowMs: 60_000, max: 100 },                 // 默认:1 分钟 100 次
}
 
export function getRateLimitConfig(path: string) {
  // 精确匹配 → 前缀匹配 → 默认
  return RATE_LIMITS[path]
    || Object.entries(RATE_LIMITS).find(([k]) => path.startsWith(k))?.[1]
    || { windowMs: 60_000, max: 100 }
}

6. 安全头中间件

除了 CORS,生产环境还需要一系列安全响应头:

// server/middleware/05.security-headers.ts
export default defineEventHandler((event) => {
  // 仅对页面请求设置(API 不需要)
  if (event.path.startsWith('/api/')) return
 
  setResponseHeaders(event, {
    // 防止 XSS
    'X-Content-Type-Options': 'nosniff',
    'X-XSS-Protection': '1; mode=block',
 
    // 防止点击劫持
    'X-Frame-Options': 'SAMEORIGIN',
 
    // 强制 HTTPS
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
 
    // 控制 Referrer 泄露
    'Referrer-Policy': 'strict-origin-when-cross-origin',
 
    // 权限策略:禁用不需要的浏览器功能
    'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
  })
})

如果项目较简单,也可以直接用 nuxt-security 模块,它封装了上述所有安全头配置:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-security'],
  security: {
    headers: {
      crossOriginEmbedderPolicy: 'unsafe-none',  // 允许嵌入外部视频
    },
  },
})

7. 请求 ID 与链路追踪

微服务场景下,每个请求需要一个唯一 ID 来贯穿日志链路:

// server/middleware/00.request-id.ts
import { randomUUID } from 'node:crypto'
 
export default defineEventHandler((event) => {
  // 如果上游(nginx/CDN)已经设置了 Request-ID,复用它
  const existing = getRequestHeader(event, 'x-request-id')
  const requestId = existing || randomUUID()
 
  // 注入到 event.context,供后续中间件和 handler 使用
  event.context.requestId = requestId
 
  // 设置到响应头,方便客户端排查问题
  setResponseHeader(event, 'X-Request-ID', requestId)
})

在日志中间件中可以结合使用:

// server/middleware/01.logger.ts
export default defineEventHandler((event) => {
  const start = Date.now()
  const requestId = event.context.requestId || '-'
 
  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    const status = event.node.res.statusCode
    // 结构化日志,方便 ELK/Loki 检索
    console.log(JSON.stringify({
      requestId,
      method: event.method,
      path: event.path,
      status,
      duration,
      ip: getRequestIP(event, { xForwardedFor: true }),
      userAgent: getRequestHeader(event, 'user-agent'),
      timestamp: new Date().toISOString(),
    }))
  })
})

8. 中间件 vs routeRules vs Nitro 钩子

三种机制都能影响请求处理,但适用场景不同:

维度server/middleware/routeRulesNitro 钩子
配置位置server/middleware/*.tsnuxt.config.tsserver/plugins/*.ts
粒度可按路径判断按路径模式匹配全局所有请求
能力读写请求/响应、拦截缓存、重定向、CORS、头监控、日志
能否拦截请求✅(重定向)
代码复杂度高(自定义逻辑)低(声明式配置)
Edge 兼容

选型建议

  • 能用 routeRules 的优先用 routeRules——声明式、零代码、CDN 可感知
  • 需要自定义逻辑的用 middleware——认证、限流、请求改写
  • 基础设施级监控用 hooks——全局错误上报、性能追踪

8.1 避免中间件误区

// ❌ 误区 1:在中间件中做路由级别的业务逻辑
export default defineEventHandler(async (event) => {
  if (event.path === '/api/videos' && event.method === 'POST') {
    const body = await readBody(event)
    // 大量业务代码...
  }
})
// → 应该放在 server/api/videos/index.post.ts 中
 
// ❌ 误区 2:每个中间件都读取 readBody
// readBody 只能读取一次!多个中间件读取会导致后续为空
// → 如果需要预处理请求体,存入 event.context
 
// ❌ 误区 3:中间件中抛出非 createError 的错误
throw new Error('something wrong')  // 客户端会收到 500 + 堆栈信息(不安全)
// → 始终用 createError({ statusCode, message }) 抛出可控错误

9. 完整请求管道图

将所有内容串联,一个请求在 Nitro 中的完整生命周期:

客户端请求
  ↓
[Nitro 钩子: request]        ← 注入 requestId、开始计时
  ↓
[middleware/00.request-id.ts] ← 生成 X-Request-ID
[middleware/01.logger.ts]     ← 注册 finish 回调
[middleware/02.cors.ts]       ← 处理跨域(OPTIONS 直接返回)
[middleware/03.auth.ts]       ← JWT 验证 → event.context.user
[middleware/04.rate-limit.ts] ← IP 限流检查
[middleware/05.security.ts]   ← 安全响应头
  ↓
[routeRules 匹配]            ← 缓存命中?直接返回
  ↓
[API handler]                ← 业务逻辑执行
  ↓
[Nitro 钩子: beforeResponse]  ← 最后修改响应的机会
  ↓
响应发送给客户端
  ↓
[Nitro 钩子: afterResponse]   ← 日志记录、清理资源

本章小结

  • 执行模型:中间件按文件名字母序执行,不返回值则放行,返回值或抛错则拦截
  • 数据传递event.context 在中间件与 handler 之间共享数据
  • JWT 认证:中间件提取 Token → 验证 → 注入 event.context.user,配合 requireAuth 工具函数
  • 请求日志:利用 res.on('finish') 记录耗时和状态码,结构化 JSON 格式
  • CORS:简单场景用 routeRules 配置,复杂场景用中间件动态设置
  • 限流:基于 IP 的滑动窗口计数,支持分级策略,生产环境建议用 Redis
  • 安全头X-Content-Type-Options / X-Frame-Options / HSTS 等生产必备
  • 请求 IDX-Request-ID 贯穿日志链路,方便微服务场景下问题排查
  • 三种机制选型:routeRules(声明式)> middleware(自定义逻辑)> hooks(基础设施)