Skip to content

服务中间件

server/middleware/ 下的文件在每个请求到达路由前执行,用于请求拦截、认证、日志等横切关注点。

中间件 vs 路由 vs 插件

概念执行时机作用示例
服务插件服务器启动时初始化连接、定时任务数据库连接、Redis 初始化
服务中间件每个请求时请求拦截、认证、日志JWT 验证、CORS、限流
API/路由匹配路由时处理业务逻辑用户 CRUD、搜索

请求处理流程

text
请求 → 中间件 1 → 中间件 2 → ... → 路由处理 → 响应
↓           ↓                  ↓
CORS 检查   认证检查          业务逻辑

中间件可以在请求到达路由前拦截、修改请求,或在路由处理后修改响应。

基本用法

ts
// server/middleware/log.ts
export default defineEventHandler((event) => {
  console.log(`[${event.method}] ${getRequestURL(event)}`)
})

中间件的执行特点

  • 不返回值:中间件执行完后,请求继续传递给下一个中间件或路由
  • 抛出错误throw createError() 会中断请求,直接返回错误响应
  • 修改 event.context:可以在中间件中设置数据,后续中间件和路由可以读取

认证中间件

ts
// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const url = getRequestURL(event)

  // 仅对 /api 路由检查认证
  if (url.pathname.startsWith('/api')) {
    const token = getRequestHeader(event, 'authorization')

    if (!token) {
      throw createError({
        statusCode: 401,
        statusMessage: 'Unauthorized',
        message: '未提供认证凭证',
      })
    }

    // 验证 token
    try {
      const payload = verifyToken(token.replace('Bearer ', ''))
      event.context.user = payload  // 设置上下文,后续可用
    } catch {
      throw createError({
        statusCode: 401,
        statusMessage: 'Unauthorized',
        message: 'Token 无效或已过期',
      })
    }
  }
})

认证中间件的设计要点

  1. 只对需要认证的路由检查——通过 URL 前缀或白名单判断
  2. 将用户信息存入 event.context——后续路由直接读取
  3. 抛出错误中断请求——不需要手动写响应

CORS 中间件

ts
// server/middleware/cors.ts
export default defineEventHandler((event) => {
  setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
  setResponseHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  setResponseHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization')
  setResponseHeader(event, 'Access-Control-Max-Age', '86400')  // 预检缓存 24 小时

  // 处理预检请求
  if (event.method === 'OPTIONS') {
    setResponseStatus(event, 204)
    return ''
  }
})

CORS 为什么需要中间件?

  • 浏览器跨域请求会先发送 OPTIONS 预检请求
  • 需要在所有 API 响应中添加 Access-Control-*
  • 用中间件统一处理,避免在每个路由中重复设置

更简单的方案

nuxt.config.ts 中用 routeRules 配置 CORS:

ts
export default defineNuxtConfig({
routeRules: {
'/api/**': { cors: true },
},
})

限流中间件

ts
// server/middleware/rate-limit.ts
const requestCounts = new Map<string, { count: number; resetTime: number }>()

export default defineEventHandler((event) => {
  const ip = getRequestHeader(event, 'x-forwarded-for') || 'unknown'
  const now = Date.now()
  const windowMs = 60 * 1000 // 1 分钟
  const maxRequests = 100

  const record = requestCounts.get(ip)

  if (!record || now > record.resetTime) {
    requestCounts.set(ip, { count: 1, resetTime: now + windowMs })
    return
  }

  record.count++

  if (record.count > maxRequests) {
    throw createError({
      statusCode: 429,
      statusMessage: 'Too Many Requests',
      message: '请求过于频繁,请稍后再试',
    })
  }

  setResponseHeader(event, 'X-RateLimit-Limit', maxRequests.toString())
  setResponseHeader(event, 'X-RateLimit-Remaining', (maxRequests - record.count).toString())
})

INFO

此限流方案仅适用于单实例部署 多实例部署时,内存中的计数器不共享,需要使用 Redis 等共享存储

请求计时中间件

ts
// server/middleware/timing.ts
export default defineEventHandler((event) => {
  const start = Date.now()

  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    setResponseHeader(event, 'X-Response-Time', `${duration}ms`)
    console.log(`[${event.method}] ${event.path} - ${duration}ms`)
  })
})

中间件执行顺序

文件名按字母顺序执行,建议使用数字前缀控制顺序:

text
server/middleware/
├── 01.cors.ts       → 最先执行:处理跨域
├── 02.auth.ts       → 其次:认证检查
├── 03.rate-limit.ts → 再次:限流检查
└── log.ts           → 最后:日志记录

执行顺序的重要性

  • CORS 必须在认证之前——否则预检请求会被认证拦截
  • 限流通常在认证之前——避免未认证请求浪费服务器资源
  • 日志通常在最后——记录所有请求(包括被拦截的)

推荐命名规范

  • 01.cors.ts → 跨域处理
  • 02.rate-limit.ts → 限流
  • 03.auth.ts → 认证
  • 99.log.ts → 日志

event.context 使用

中间件中设置的数据,后续中间件和路由可以读取:

ts
// server/middleware/auth.ts
export default defineEventHandler((event) => {
  event.context.user = { id: 1, role: 'admin' }
  event.context.db = getDbConnection()
})

// server/api/me.ts
export default defineEventHandler((event) => {
  const user = event.context.user  // 直接读取
  return user
})

event.context 是中间件和路由之间的数据桥梁

  • 中间件设置数据(如用户信息、数据库连接)
  • 路由读取数据,无需重复获取
  • 避免在每个路由中重复认证/初始化逻辑

类型扩展

event.context 添加 TypeScript 类型声明:

ts
// server/types/index.d.ts
declare module 'h3' {
  interface H3EventContext {
    user?: { id: number; role: string }
    db?: Database
  }
}

export {}

类型扩展后

在所有服务端文件中 event.context.userevent.context.db 都有正确的类型提示。

注意事项

  1. 中间件不应返回响应:仅检查和扩展请求上下文。如果需要返回响应,用 throw createError() 或使用路由。
  2. 每个请求都会执行:注意性能影响,避免在中间件中做耗时操作(如数据库查询)。
  3. 错误会中断请求throw createError() 会阻止后续中间件和路由的执行。
  4. 共享 event.context:可以在中间件中设置数据,路由中读取。
  5. 避免循环依赖:中间件之间不要互相引用。

知识脉络

text
API 路由 → 服务路由 → 你在这里:服务中间件

                          ├─→ 下一步:服务插件

                          └─→ 相关:错误处理(createError 详解)

基于 Nuxt 4 官方文档整理编写