18.10-IP级限流

要点

  • IP 级限流是防滥用的第一道防线——不需要登录就能生效,挡住匿名攻击
  • Cloudflare 自动加 CF-Connecting-IP 头——这是最可靠的客户端 IP 来源,不要信任 X-Forwarded-For
  • IPv6 限流要小心——同一用户可能用多个 IPv6 地址,按 /48 子网限流更合理
  • 实现:KV 做高速计数,D1 做持久化黑名单
  • 常见绕过手法:代理池、IPv6 轮换、Header 伪造——应对策略不同
  • IP 限流通常跟用户级限流叠加——匿名请求按 IP,登录请求按 user_id

1. 为什么需要 IP 限流

用户级限流需要登录才能生效。但很多接口是公开的:

  • 注册/登录接口——防止撞库攻击
  • 密码重置——防止邮件轰炸
  • 公开查询接口——防止被爬虫刷爆
  • 上传接口——防止恶意大文件上传

这些接口需要 IP 级限流——不依赖登录状态,任何请求都能限制。

2. 获取真实 IP

2.1 Cloudflare 的 CF-Connecting-IP

请求经过 Cloudflare 后,Cloudflare 会在请求头里加 CF-Connecting-IP——这是客户端的真实 IP(Cloudflare 从 TCP 连接拿的,无法伪造):

app.get('/api/public', (c) => {
  const clientIP = c.req.header('CF-Connecting-IP')
  // 这是真实 IP,Cloudflare 保证准确
  return c.json({ yourIp: clientIP })
})

2.2 不要信任 X-Forwarded-For

X-Forwarded-For 是客户端可以随便改的 header:

curl -H 'X-Forwarded-For: 1.2.3.4' https://api.example/

如果你从 X-Forwarded-For 取 IP,攻击者可以随便指定 IP 绕过限流。

CF-Connecting-IP 是 Cloudflare 根据 TCP 连接加的,客户端无法伪造。

2.3 不经过 Cloudflare 的情况

如果 Worker 不经过 Cloudflare 代理(直接暴露 IP),CF-Connecting-IP 不存在。此时从 TCP 连接拿 IP——但 Worker 环境没有直接暴露这个信息的 API。

一般建议:Worker 必须走 Cloudflare 代理,既保证安全又能拿到真实 IP。

3. IP 限流实现

3.1 Hono 中间件

// src/middleware/ip-rate-limit.ts
import { Context, Next } from 'hono'
 
type IPRateLimitConfig = {
  limit: number
  windowSeconds: number
  keyFn?: (ip: string) => string  // 自定义 key 生成(比如按子网)
}
 
export function ipRateLimit(config: IPRateLimitConfig) {
  return async (c: Context, next: Next) => {
    const clientIP = c.req.header('CF-Connecting-IP')
    if (!clientIP) {
      // 没有 Cloudflare IP 头,可能是绕过代理的直接访问
      return c.json({ error: 'Access denied' }, 403)
    }
 
    const key = config.keyFn ? config.keyFn(clientIP) : `ip:${clientIP}`
    const result = await checkIPLimit(c.env, key, config)
 
    if (!result.allowed) {
      c.header('Retry-After', String(config.windowSeconds))
      c.header('X-RateLimit-Limit', String(config.limit))
      c.header('X-RateLimit-Remaining', '0')
      return c.json({ error: 'Too many requests' }, 429)
    }
 
    c.header('X-RateLimit-Limit', String(config.limit))
    c.header('X-RateLimit-Remaining', String(result.remaining))
    await next()
  }
}
 
async function checkIPLimit(
  env: { RATE_LIMIT_KV: KVNamespace },
  key: string,
  config: IPRateLimitConfig
) {
  const window = Math.floor(Date.now() / 1000 / config.windowSeconds)
  const windowKey = `${key}:${window}`
 
  const current = parseInt((await env.RATE_LIMIT_KV.get(windowKey)) || '0')
 
  if (current >= config.limit) {
    return { allowed: false, remaining: 0 }
  }
 
  await env.RATE_LIMIT_KV.put(windowKey, String(current + 1), {
    expirationTtl: config.windowSeconds * 2,
  })
 
  return { allowed: true, remaining: config.limit - current - 1 }
}

3.2 使用

// 公开接口:每 IP 每分钟 20 次
app.use('/api/public/*', ipRateLimit({
  limit: 20,
  windowSeconds: 60,
}))
 
// 注册接口:每 IP 每小时 5 次
app.use('/api/auth/register', ipRateLimit({
  limit: 5,
  windowSeconds: 3600,
}))
 
// 密码重置:每 IP 每天 3 次
app.use('/api/auth/reset-password', ipRateLimit({
  limit: 3,
  windowSeconds: 86400,
}))

4. IPv6 限流的问题

IPv6 地址空间巨大,ISP 通常给每个用户分配一个 /48 或 /56 子网。同一用户每次连接可能用不同的 IPv6 地址:

2001:db8:1234:5678::1
2001:db8:1234:5678::2
2001:db8:1234:5678::3

如果按完整 IPv6 地址限流,攻击者可以用不同地址绕过。

4.1 按 IPv6 子网限流

function normalizeIPv6(ip: string): string {
  // IPv4 直接返回
  if (!ip.includes(':')) return ip
 
  // IPv6 按 /48 子网限流(前 3 段)
  const parts = ip.split(':')
  return parts.slice(0, 3).join(':') + '::/48'
}
 
// 使用
app.use('/api/public/*', ipRateLimit({
  limit: 20,
  windowSeconds: 60,
  keyFn: (ip) => `ip:${normalizeIPv6(ip)}`,
}))

4.2 IPv4 和 IPv6 统一处理

function ipToLimitKey(ip: string): string {
  if (ip.includes(':')) {
    // IPv6:按 /48
    const parts = ip.split(':')
    return parts.slice(0, 3).join(':')
  } else {
    // IPv4:完整地址
    return ip
  }
}

5. 绕过与防御

5.1 代理池

攻击者用代理池,每次请求用不同 IP。单 IP 限流完全无效。

防御:

  • 识别数据中心 IP 段(proxylists、IPinfo 数据库)
  • 数据中心 IP 用更严格的限制
  • Cloudflare Bot Fight Mode 自动拦截已知代理
async function isDatacenterIP(ip: string): Promise<boolean> {
  // 查 IPinfo 或本地数据库
  const record = await env.IP_DB.get(`ip-info:${ip}`)
  if (!record) return false
  const info = JSON.parse(record)
  return info.type === 'hosting' || info.type === 'vpn'
}
 
// 数据中心 IP 限流更严格
app.use('/api/public/*', async (c, next) => {
  const ip = c.req.header('CF-Connecting-IP')!
  const limit = await isDatacenterIP(ip) ? 5 : 20
  return ipRateLimit({ limit, windowSeconds: 60 })(c, next)
})

5.2 Header 伪造

攻击者伪造 X-Forwarded-For 绕过 IP 限流。

防御:只用 CF-Connecting-IP,不读 X-Forwarded-For

5.3 IPv6 轮换

攻击者用同一 /48 下的不同 IPv6 地址。

防御:按 /48 子网限流(前面已讲)。

5.4 慢速攻击(Slowloris)

攻击者用极慢的速率发请求,不触发限流但持续占用连接。

防御:Cloudflare 自动处理大部分慢速攻击。Worker 本身是无状态的,不怕慢速连接。

6. IP 黑名单

某些 IP 行为恶劣(频繁攻击、爬虫),直接加黑名单拒绝所有请求:

// src/middleware/ip-blacklist.ts
export async function ipBlacklist(c: Context, next: Next) {
  const ip = c.req.header('CF-Connecting-IP')
  if (!ip) return c.json({ error: 'Access denied' }, 403)
 
  const blocked = await c.env.BLACKLIST_KV.get(`blocked:${ip}`)
  if (blocked) {
    return c.json({ error: 'Access denied' }, 403)
  }
 
  await next()
}
 
// 加黑名单(管理员接口)
app.post('/admin/blacklist/:ip', async (c) => {
  const ip = c.req.param('ip')
  const reason = (await c.req.json()).reason || 'abuse'
 
  await c.env.BLACKLIST_KV.put(`blocked:${ip}`, reason, {
    expirationTtl: 86400 * 7,  // 7 天后自动解除
  })
 
  return c.json({ ok: true })
})

6.1 自动加黑名单

根据限流触发频率自动加黑名单:

// 每次限流触发时记录
async function recordRateLimitHit(env: Env, ip: string) {
  const key = `ratelimit-hits:${ip}:${Math.floor(Date.now() / 86400)}`
  const count = parseInt((await env.RATE_LIMIT_KV.get(key)) || '0') + 1
  await env.RATE_LIMIT_KV.put(key, String(count), { expirationTtl: 86400 * 2 })
 
  // 一天内触发 100 次以上,自动加黑名单
  if (count >= 100) {
    await env.BLACKLIST_KV.put(`blocked:${ip}`, 'auto:blocked', {
      expirationTtl: 86400 * 7,
    })
  }
}

7. IP 限流 vs 用户限流

维度IP 限流用户限流
生效条件无需登录需要登录
准确度中(NAT、代理会影响)高(user_id 唯一)
适用接口公开接口(注册、登录、公开查询)需要鉴权的接口
绕过难度中(代理池可绕过)低(要注册多个账号)
成本低(KV 计数)中(D1 查配额)

实际部署通常两者叠加:

// 公开接口:IP 限流
app.use('/api/auth/*', ipRateLimit({ limit: 20, windowSeconds: 60 }))
 
// 登录后的接口:用户限流 + IP 限流
app.use('/api/ai/*', ipRateLimit({ limit: 100, windowSeconds: 60 }))
app.use('/api/ai/*', userRateLimit)  // 按 user_id 限

两层都通过才放行——匿名攻击被 IP 限流挡住,登录后的滥用被用户限流限制。

8. Cloudflare 的额外保护

Cloudflare 自带一些防护能力,不需要在 Worker 里重复实现:

  • Bot Fight Mode:自动识别和拦截机器人
  • WAF:Web 应用防火墙,拦截常见攻击模式
  • DDoS 保护:大规模流量攻击自动吸收
  • Rate Limiting Rules:Cloudflare Dashboard 里配置限流规则,在边缘就拦截,根本不到 Worker

Cloudflare Rate Limiting Rules 可以在 Dashboard → Security → WAF → Rate limiting rules 配置。好处是限流在边缘执行,节省 Worker 计算资源。

请求 → Cloudflare Rate Limit(边缘)→ Worker Rate Limit(应用层)→ 业务

边缘限流做粗粒度(每 IP 每分钟 100 次),应用层做细粒度(每用户每天配额)。

9. 小结

IP 级限流的核心:

  • 真实 IP:用 CF-Connecting-IP,不要信任 X-Forwarded-For
  • IPv6:按 /48 子网限流,避免同一用户被多 IP 绕过
  • 实现:KV 做高速计数,固定窗口或滑动窗口算法
  • 绕过防御:代理池用数据中心 IP 识别,Header 伪造靠只用 CF-Connecting-IP,IPv6 轮换靠子网限流
  • 黑名单:根据限流触发频率自动加黑名单
  • 与用户限流叠加:公开接口按 IP,登录接口按 user_id,两层都过才放行
  • Cloudflare 边缘限流:粗粒度在边缘,细粒度在应用层

下一篇讲模型调用限流——按 token 数/请求数限流、分级限流(按 model)、成本预算控制。