服务中间件
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 无效或已过期',
})
}
}
})认证中间件的设计要点
- 只对需要认证的路由检查——通过 URL 前缀或白名单判断
- 将用户信息存入
event.context——后续路由直接读取 - 抛出错误中断请求——不需要手动写响应
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.user 和 event.context.db 都有正确的类型提示。
注意事项
- 中间件不应返回响应:仅检查和扩展请求上下文。如果需要返回响应,用
throw createError()或使用路由。 - 每个请求都会执行:注意性能影响,避免在中间件中做耗时操作(如数据库查询)。
- 错误会中断请求:
throw createError()会阻止后续中间件和路由的执行。 - 共享 event.context:可以在中间件中设置数据,路由中读取。
- 避免循环依赖:中间件之间不要互相引用。
知识脉络
text
API 路由 → 服务路由 → 你在这里:服务中间件
│
├─→ 下一步:服务插件
│
└─→ 相关:错误处理(createError 详解)