package service import ( "strings" "sync" "time" ) // maxGuardEntries 是 failures / lockedAt 各自的条目上限(S-03): // 攻击者持续提交唯一用户名时先清过期、仍满驱逐最旧,内存有界。 const maxGuardEntries = 4096 // guardUserMax 是计入守卫 key 的用户名字节上限,超长截断防高基数撑爆。 const guardUserMax = 32 // loginGuard 按「IP|用户名」双维度做滑动窗口失败计数与锁定; // 窗口与锁定时长同值、阈值由调用方按安全设置传入(探索文档主题三); // 内存实现(单体面板),重启清零可接受。 type loginGuard struct { mu sync.Mutex failures map[string][]time.Time lockedAt map[string]time.Time } func newLoginGuard() *loginGuard { return &loginGuard{ failures: map[string][]time.Time{}, lockedAt: map[string]time.Time{}, } } // guardKey 规范化守卫键:用户名去首尾空白并按字节截断, // 高基数/超长输入不会产生无界的独立条目。 func guardKey(clientIP, username string) string { name := strings.TrimSpace(username) if len(name) > guardUserMax { name = name[:guardUserMax] } return clientIP + "|" + name } // locked 判定 key 是否处于锁定期;过期锁惰性清除。 func (g *loginGuard) locked(key string, now time.Time, lockFor time.Duration) bool { g.mu.Lock() defer g.mu.Unlock() at, ok := g.lockedAt[key] if !ok { return false } if now.Sub(at) >= lockFor { delete(g.lockedAt, key) return false } return true } // fail 记一次失败并裁剪窗口;达到阈值转入锁定并返回 true(仅触发那一次)。 // 新建条目前先保证容量(过期清理 + 最旧驱逐),防高基数 key 单向增长。 func (g *loginGuard) fail(key string, now time.Time, limit int, window time.Duration) bool { g.mu.Lock() defer g.mu.Unlock() if _, exists := g.failures[key]; !exists { g.ensureCapacity(now, window) } kept := g.failures[key][:0] for _, t := range g.failures[key] { if now.Sub(t) < window { kept = append(kept, t) } } kept = append(kept, now) if len(kept) >= limit { delete(g.failures, key) // 转入锁定同样要保证容量:该写入发生在已有 failures 条目的 // 第 N 次失败,不经过新建条目路径的 ensureCapacity g.ensureLockCapacity(now, window) g.lockedAt[key] = now return true } g.failures[key] = kept return false } // ensureLockCapacity 在写入 lockedAt 前保证容量:先清已过锁定期的锁, // 仍满驱逐锁定最早的条目(其剩余锁定时间最短,提前解锁的影响最小)。 func (g *loginGuard) ensureLockCapacity(now time.Time, lockFor time.Duration) { if len(g.lockedAt) < maxGuardEntries { return } for k, at := range g.lockedAt { if now.Sub(at) >= lockFor { delete(g.lockedAt, k) } } if len(g.lockedAt) >= maxGuardEntries { g.evictOldestLock() } } // evictOldestLock 驱逐锁定时间最早的 lockedAt 条目(调用方须持锁)。 func (g *loginGuard) evictOldestLock() { var oldestKey string var oldestAt time.Time for k, at := range g.lockedAt { if oldestKey == "" || at.Before(oldestAt) { oldestKey, oldestAt = k, at } } if oldestKey != "" { delete(g.lockedAt, oldestKey) } } // ensureCapacity 在新建 failures 条目前保证容量:先清出窗口外的失败记录 // 与已过锁定期的锁,仍满则驱逐最后失败时间最早的条目(调用方须持锁)。 func (g *loginGuard) ensureCapacity(now time.Time, window time.Duration) { if len(g.failures) < maxGuardEntries && len(g.lockedAt) < maxGuardEntries { return } for k, times := range g.failures { if len(times) == 0 || now.Sub(times[len(times)-1]) >= window { delete(g.failures, k) } } for k, at := range g.lockedAt { if now.Sub(at) >= window { delete(g.lockedAt, k) } } if len(g.failures) >= maxGuardEntries { g.evictOldest() } } // evictOldest 驱逐最后失败时间最早的 failures 条目(调用方须持锁); // 仅在容量兜底时触发,O(n) 扫描可接受。 func (g *loginGuard) evictOldest() { var oldestKey string var oldestAt time.Time for k, times := range g.failures { if len(times) == 0 { delete(g.failures, k) continue } last := times[len(times)-1] if oldestKey == "" || last.Before(oldestAt) { oldestKey, oldestAt = k, last } } if oldestKey != "" { delete(g.failures, oldestKey) } } // success 清空 key 的失败计数(成功登录重置窗口)。 func (g *loginGuard) success(key string) { g.mu.Lock() defer g.mu.Unlock() delete(g.failures, key) }