单用incr挡不住暴力破解,因incr虽原子,但后续判断和expire非原子,存在竞态;必须用lua脚本将自增、判断、设过期三步封装为一个原子操作,由redis串行执行,彻底规避竞态。

直接用 redis.Incr 做登录失败计数会漏统计并发请求,根本挡不住暴力破解。必须用 Lua 脚本把自增、判断、设过期三步原子执行。
为什么单用 INCR 挡不住爆破
多个登录请求几乎同时到达时,redis.Incr 虽然自身原子,但后续的 if current > limit 和 redis.Expire 不在同一个原子上下文中。中间可能插入其他请求,导致同一分钟内实际失败 12 次,却只拦截了前 10 次——剩下 2 次绕过检查,继续尝试。
- 现象:日志里看到“第 11 次失败”仍返回 200,用户还能继续输密码
- 本质:竞态条件(race condition),不是 Redis 性能问题,是逻辑断层
- 误区:以为 “Redis 命令原子 = 整个限流逻辑原子”,其实只是单命令原子
用 Lua 脚本实现原子限流
把计数、阈值判断、过期设置全部压进一个脚本,由 Redis 串行执行,彻底规避竞态。脚本返回值即当前计数,>0 表示放行,0 表示拒绝。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
if current > tonumber(ARGV[2]) then
return 0
end
return current
-
KEYS[1]是 key,例如"login:fail:192.168.1.100"或"login:fail:email:user@example.com" -
ARGV[1]是窗口秒数,比如"60"(1 分钟) -
ARGV[2]是最大失败次数,比如"5" - 必须预编译:
rateScript := redis.NewScript(luaScript),全局复用,别每次 new
Gin 中间件里怎么安全调用
不要在限流中间件里查 DB 拿用户标识——这会让所有请求都拖慢。应在认证中间件之后,把 userID 或 IP 写入上下文,此处直接取。
- 提取标识优先级建议:
c.ClientIP()→c.GetHeader("X-Real-IP")→ fallback 到c.Request.RemoteAddr(注意代理配置) - Key 拼接带业务前缀:
"login:fail:ip:" + ip,避免和接口限流 key 冲突 - 调用时必须带 context timeout:
ctx, cancel := context.WithTimeout(c.Request.Context(), 300*time.Millisecond) - 错误处理区分:
errors.Is(err, redis.Nil)不该出现(Lua 总返回),真正要捕获的是网络超时或连接错误
配套措施不能少
限流只是防爆破的一环,单独用效果有限。关键配合点容易被忽略:
- 登录成功后,必须立刻
DEL login:fail:xxx,否则用户重试几次就永久锁死 - 密码校验必须用
bcrypt.CompareHashAndPassword,不能明文比对,否则爆破可绕过限流直击 DB - 前端要加验证码(如
base64Captcha),且验证码 ID 必须和服务端存储绑定,验证后立即失效 - Redis 连接池要调优:
PoolSize至少 20,MinIdleConns设为 5,否则高并发下连接池打满,限流本身变成瓶颈
真正卡住暴力破解的,不是某一行代码,而是 IP 标识提取是否可靠、Lua 脚本是否真原子、失败计数是否清零及时、以及密码校验有没有被绕过——四个点缺一不可。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










