🛡️ 使用 Redis + Lua 实现每分钟登录尝试次数限制,防止暴力破解

204 阅读2分钟

📖 模块简介

在金融等高安全性场景下,登录接口极易成为暴力破解攻击的目标。通过 Redis 的高性能计数器和 Lua 脚本原子性,可以实现每分钟登录尝试次数限制,及时阻断异常行为,提升系统安全性。本文系统介绍其原理、应用场景、Go+Redis+Lua 实现、常见问题与最佳实践。


🧠 基础原理

  1. 计数器限流:每个用户(或 IP)登录尝试时,使用 Redis 的 INCR/EXPIRE 或 Lua 脚本对尝试次数进行原子性累加。
  2. 时间窗口:以分钟为单位设置计数器过期时间,自动滑动时间窗口。
  3. Lua 脚本原子操作:用 Lua 保证加计数和设置过期的原子性,防止并发下计数器失效。
  4. 超限阻断:当计数器超过阈值(如 5 次/分钟)即拒绝登录,返回友好提示。

💼 金融业务应用场景

  • 账户登录防暴力破解:限制每个账户/手机号/设备每分钟登录尝试次数,防止被撞库。
  • 支付/敏感操作防刷:对敏感操作接口(如支付、提现)做频率限制。
  • API 接口防刷限流:对外开放 API 做细粒度限流,防止恶意攻击。

💻 示例代码(Go + Redis + Lua)

1. Lua 脚本(每分钟限 5 次)

-- KEYS[1]: 计数器 key(如 login:attempt:userid)
-- ARGV[1]: 限制次数(如5)
-- ARGV[2]: 过期时间(秒,60)
local cnt = redis.call('INCR', KEYS[1])
if cnt == 1 then
    redis.call('EXPIRE', KEYS[1], ARGV[2])
end
if cnt > tonumber(ARGV[1]) then
    return 0 -- 超限
else
    return 1 -- 未超限
end

2. Go 端调用示例

script := `
local cnt = redis.call('INCR', KEYS[1])
if cnt == 1 then
    redis.call('EXPIRE', KEYS[1], ARGV[2])
end
if cnt > tonumber(ARGV[1]) then
    return 0
else
    return 1
end`

userID := "u1001"
key := fmt.Sprintf("login:attempt:%s", userID)
limit := 5
expire := 60 // 秒
res, err := rdb.Eval(ctx, script, []string{key}, limit, expire).Result()
if err != nil {
    // Redis 异常处理
}
if res.(int64) == 0 {
    // 超过限制,拒绝登录
    return errors.New("登录过于频繁,请稍后再试")
}
// 继续登录逻辑

🚨 常见问题与注意事项

  • 分布式部署一致性:多节点需共用同一 Redis,防止绕过限制。
  • Lua 脚本返回类型:注意 Go 端类型断言,避免类型不匹配。
  • 恶意用户绕过:建议对用户ID和IP双重限流,提升防护。
  • Redis 持久化与高可用:Redis 故障可能导致限流失效,需保障高可用。
  • 误伤正常用户:阈值设置需平衡安全与用户体验。

✅ 最佳实践建议

  1. 用户ID与IP双重限流,提升防护深度。
  2. 阈值设置结合业务实际,避免误伤。
  3. Redis 需高可用部署,防止单点故障。
  4. 结合黑名单、验证码等多重防护措施。
  5. 定期监控限流命中率,动态调整策略。

📚 延伸阅读