fix(security): 修复 P4 封禁记录数据错位 + P5 枚举爆破宽限(方案 A)

P4 #17 手动封禁 Create 非 upsert 导致数据错位:
- BanStore 新增 BanIP(ip, reason, duration):事务内清理该 IP 全部
  既有记录(兼容历史脏数据)后插入单条封禁记录,计数清零;
  DisconnectConnection 改用(原裸 Create 为全仓库唯一调用点)
- BanEntry.IPAddress 升级 uniqueIndex;InitDB 在 AutoMigrate 前
  dedupeBanEntries 清理历史重复行(MySQL 1093 兼容写法)
- IncrementFail 原子化:SQL 侧 fail_count+1,miss 时 OnConflict
  DoNothing 插入兜底并发竞态,回读计数

P5 #18 方案 A(按失败性质区分宽限):
- RecordAuthFailure 新增 knownUser 参数:用户名不存在(枚举型
  爆破)跳过 3 次宽限、首次触发即封第 1 档;用户名存在(真实
  用户输错)保留宽限防误封
- 新增 UserStore.LoginExists(邮箱/裸用户名);五个失败调用点
  接线(Web 登录查存在性;SMTP/IMAP/POP3 用登录名;LDAP 侧
  存在性不可判定,保守按已知用户处理)
- 封禁原因注明「未知用户名,跳过宽限」便于后台审计

新增 8 个测试(-race 通过):BanIP 单行 upsert、唯一索引约束、
16 协程并发计数精确、dedupe 清理/表不存在静默、未知用户即时
封禁、已知用户宽限回归、LoginExists 矩阵。
This commit is contained in:
2026-08-20 11:25:10 +08:00
parent 37b4816573
commit 6ee2dd8184
13 changed files with 481 additions and 47 deletions
+4 -8
View File
@@ -79,14 +79,10 @@ func (h *AdminHandler) DisconnectConnection(c *gin.Context) {
return
}
// 加入黑名单:180 天封禁(管理员可随时解封)
if err := h.stores.Bans.Create(&db.BanEntry{
IPAddress: conn.IP,
Reason: "管理员手动封禁(连接断开)",
FailCount: 0,
BanCount: 0,
ExpiresAt: time.Now().Add(manualBanDuration),
}); err != nil {
// 加入黑名单:180 天封禁(管理员可随时解封)
// BanIP 为 upsert 语义:清理该 IP 既有观察/重复记录后仅保留一条,
// 避免与阶段性封禁的计数/档位记录错位。
if err := h.stores.Bans.BanIP(conn.IP, "管理员手动封禁(连接断开)", manualBanDuration); err != nil {
c.String(http.StatusInternalServerError, "封禁失败: %v", err)
return
}