package auth import ( "sync" "time" ) // Limiter 是按键(IP / 用户)计数的滑动窗口限流器。 // 进程内内存实现:现场单实例部署足够,不引入额外存储。 type Limiter struct { mu sync.Mutex hits map[string][]time.Time limit int window time.Duration now func() time.Time } // NewLimiter 创建限流器;limit <= 0 表示不限流。 func NewLimiter(limit int, window time.Duration, now func() time.Time) *Limiter { if now == nil { now = time.Now } return &Limiter{hits: make(map[string][]time.Time), limit: limit, window: window, now: now} } // sweepThreshold 是触发全表清理的键数量。 // // hits 只在「这个键被再次访问」时才修剪自己的时间片,所以一个只来一次的 // 随机 IP(或伪造 X-Forwarded-For 的扫描器)留下的空键会一直躺在 map 里: // 单机内网虽然打不满,但这个 map 没有上界本身就是隐患,超过阈值就顺手清一遍。 const sweepThreshold = 1024 // Allow 记录一次访问并报告是否放行。 func (l *Limiter) Allow(key string) bool { if l == nil || l.limit <= 0 { return true } now := l.now() cutoff := now.Add(-l.window) l.mu.Lock() defer l.mu.Unlock() kept := l.hits[key][:0] for _, t := range l.hits[key] { if t.After(cutoff) { kept = append(kept, t) } } if len(kept) >= l.limit { l.hits[key] = kept return false } l.hits[key] = append(kept, now) if len(l.hits) > sweepThreshold { l.sweepLocked(cutoff) } return true } // sweepLocked 删掉窗口内已经没有任何记录的键。调用方必须已持有 l.mu。 func (l *Limiter) sweepLocked(cutoff time.Time) { for key, times := range l.hits { live := false for _, t := range times { if t.After(cutoff) { live = true break } } if !live { delete(l.hits, key) } } } // Reset 清空某个键的计数,例如登录成功后不再累计失败次数。 func (l *Limiter) Reset(key string) { if l == nil { return } l.mu.Lock() delete(l.hits, key) l.mu.Unlock() }