Skip to content

自定义中间件:如何编写一个带缓存的 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 是临时的。可使用持久化存储如 CacheStorageKV

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 中间件应:

  • 使用高效数据结构(MapKV)缓存状态;
  • 正确提取客户端标识(IP、用户 ID);
  • 支持自动过期清理;
  • 在异常时优雅降级;
  • 提供清晰的反馈机制。

Hono 的中间件模型使得这类功能可以简洁、模块化地实现,既能保护你的 API,又能提升用户体验。