feat(backend): 限流/登录失败锁状态外置 Redis(todo #2)

新包 internal/ratelimit:内存/redis(GCRA+Lua) 双实现 + 出错逐调用降级内存
(fail-open 到内存不 fail-closed)。REDIS_ADDR 空=内存模式,行为与既往一致;
配置后跨重启保状态、支持多实例。miniredis 全覆盖测试,零真实外部依赖。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-05 10:42:23 +08:00
parent e850a1987c
commit 3c687e5e0b
15 changed files with 894 additions and 169 deletions
+1 -23
View File
@@ -107,29 +107,7 @@ func TestRateLimitDisabledPassthrough(t *testing.T) {
}
}
func TestKeyedLimiterSweepEvictsIdle(t *testing.T) {
kl := newKeyedLimiter(PerMinute(60), 1)
kl.get("ip:a")
kl.get("ip:b")
if len(kl.entries) != 2 {
t.Fatalf("应有 2 个 entry,得到 %d", len(kl.entries))
}
// 把 a 的活动时间推到很久以前,sweep 应只淘汰 a。
kl.mu.Lock()
kl.entries["ip:a"].lastSeen = time.Now().Add(-time.Hour)
kl.mu.Unlock()
kl.sweep(10 * time.Minute)
kl.mu.Lock()
defer kl.mu.Unlock()
if _, ok := kl.entries["ip:a"]; ok {
t.Fatal("空闲 key a 应被淘汰")
}
if _, ok := kl.entries["ip:b"]; !ok {
t.Fatal("活跃 key b 不应被淘汰")
}
}
// keyedLimiter 内部淘汰测试已随实现迁至 internal/ratelimit/memory_test.go。
// TestTrustedProxyRealIP 复刻 main.go 的可信代理配置:只信任本机写的 X-Real-IP,
// 客户端伪造的 X-Forwarded-For 不被采信 → c.ClientIP() 返回真实 IP,限流不可被请求头绕过。