服务端中间件与请求管道
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 放行
- 放行:不返回任何值(
return或return 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.user2. 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)
})
})如果需要更专业的日志,可以集成 pino 或 consola,并输出 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/ | routeRules | Nitro 钩子 |
|---|---|---|---|
| 配置位置 | server/middleware/*.ts | nuxt.config.ts | server/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等生产必备 - 请求 ID:
X-Request-ID贯穿日志链路,方便微服务场景下问题排查 - 三种机制选型:routeRules(声明式)> middleware(自定义逻辑)> hooks(基础设施)