自定义中间件:如何编写一个带缓存的 rateLimit?
在 Hono 中,你可以通过自定义中间件实现强大的功能。以下是一个带内存缓存的限流(rate limiting)中间件的完整实现,支持按 IP 或用户标识进行请求频率控制。
一、需求分析
一个实用的 rateLimit 中间件应具备:
- ✅ 基于 IP 或自定义键(如用户 ID)进行计数;
- ✅ 可配置最大请求数(
max) 和时间窗口(window); - ✅ 使用内存缓存(如
Map或平台存储)跟踪请求次数; - ✅ 到达阈值后返回
429 Too Many Requests; - ✅ 支持 TTL(过期时间)自动清理旧记录;
- ✅ 高性能,避免阻塞请求。
二、基础版本:使用 Map 实现内存缓存
ts
type RateLimitOptions = {
max?: number // 最大请求数
window?: number // 时间窗口(秒)
}
const rateLimit = (options: RateLimitOptions = {}) => {
const { max = 10, window = 60 } = options
const cache = new Map<string, { count: number; expires: number }>()
return async (c: Context, next: Next) => {
// 1. 生成限流键:使用客户端 IP
const ip = c.req.header('X-Forwarded-For')?.split(',')[0]
|| c.req.header('CF-Connecting-IP') // Cloudflare
|| c.req.ip // Hono 提供的 IP 检测
|| 'unknown'
const key = `rate:${ip}`
const now = Date.now()
const windowMs = window * 1000
const expires = now + windowMs
// 2. 获取或初始化计数
let entry = cache.get(key)
if (!entry || entry.expires <= now) {
// 过期或不存在,重置
entry = { count: 1, expires }
cache.set(key, entry)
} else {
entry.count++
cache.set(key, entry)
}
// 3. 检查是否超限
if (entry.count > max) {
return c.json(
{ error: 'Too many requests' },
429
)
}
// 4. 继续处理请求
await next()
}
}使用方式:
ts
app.use('/api/*', rateLimit({ max: 5, window: 60 })) // 每分钟最多 5 次
app.get('/api/data', (c) => c.json({ data: 'ok' }))三、优化:自动清理过期条目
上述版本可能造成内存泄漏。我们可以通过 setInterval 定期清理:
ts
const rateLimit = (options: RateLimitOptions = {}) => {
const { max = 10, window = 60 } = options
const cache = new Map<string, { count: number; expires: number }>()
const windowMs = window * 1000
// 启动定时清理(每 window/2 秒一次)
setInterval(() => {
const now = Date.now()
for (const [key, entry] of cache.entries()) {
if (entry.expires <= now) {
cache.delete(key)
}
}
}, windowMs / 2).unref() // Node.js: 不阻止进程退出
return async (c: Context, next: Next) => {
const ip = c.req.ip || 'unknown'
const key = `rate:${ip}`
const now = Date.now()
let entry = cache.get(key)
if (!entry || entry.expires <= now) {
entry = { count: 1, expires: now + windowMs }
cache.set(key, entry)
} else {
entry.count++
}
if (entry.count > max) {
return c.json({ error: 'Rate limit exceeded' }, 429)
}
await next()
}
}四、生产级:支持 CacheStorage(Cloudflare Workers)
在 Serverless 环境中,Map 是临时的。可使用持久化存储如 CacheStorage 或 KV。
ts
// 适用于 Cloudflare Workers
const rateLimitKV = (options: RateLimitOptions & { kv: KVNamespace }) => {
const { max = 10, window = 60, kv } = options
const windowMs = window * 1000
return async (c: Context, next: Next) => {
const ip = c.req.ip || 'unknown'
const key = `rate:${ip}`
try {
// 从 KV 读取当前计数
const stored = await kv.get(key, 'json')
const now = Date.now()
let count = 1
let expires = now + windowMs
if (stored && stored.expires > now) {
count = stored.count + 1
expires = stored.expires
}
// 超限则拒绝
if (count > max) {
return c.json({ error: 'Rate limit exceeded' }, 429)
}
// 更新 KV
await kv.put(key, JSON.stringify({ count, expires }), {
expirationTtl: window
})
await next()
} catch (err) {
// KV 故障时降级为放行(避免误杀)
console.warn('Rate limit KV error:', err)
await next()
}
}
}注册到应用:
ts
app.use('/api/*', rateLimitKV({ max: 100, window: 3600, kv: env.MY_KV }))五、增强功能建议
| 功能 | 实现思路 |
|---|---|
| 响应头反馈 | 添加 X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After |
| 分层限流 | 免费用户 10次/分,VIP 用户 100次/分(结合 JWT) |
| 突发控制(Burst) | 使用令牌桶算法替代计数器 |
| 日志与监控 | 记录超限事件,对接告警系统 |
示例:添加响应头
ts
if (entry.count > max) {
c.res.headers.set('X-RateLimit-Limit', String(max))
c.res.headers.set('X-RateLimit-Remaining', '0')
c.res.headers.set('Retry-After', String(window))
return c.json({ error: 'Too many requests' }, 429)
}六、总结
一个健壮的 rateLimit 中间件应:
- 使用高效数据结构(
Map、KV)缓存状态; - 正确提取客户端标识(IP、用户 ID);
- 支持自动过期清理;
- 在异常时优雅降级;
- 提供清晰的反馈机制。
Hono 的中间件模型使得这类功能可以简洁、模块化地实现,既能保护你的 API,又能提升用户体验。