package handler import ( "sync" "time" ) // 登录失败限速:同一用户名连续 N 次失败后锁定一段时间 const ( maxLoginFailures = 5 lockoutDuration = 5 * time.Minute failureWindow = 15 * time.Minute ) type failureRecord struct { count int firstSeen time.Time lockedAt time.Time } type loginLimiter struct { mu sync.Mutex recs map[string]*failureRecord } var limiter = &loginLimiter{recs: make(map[string]*failureRecord)} // Locked 返回该用户名是否处于锁定期,以及剩余秒数 func (l *loginLimiter) Locked(name string) (bool, int) { l.mu.Lock() defer l.mu.Unlock() r, ok := l.recs[name] if !ok || r.lockedAt.IsZero() { return false, 0 } elapsed := time.Since(r.lockedAt) if elapsed >= lockoutDuration { delete(l.recs, name) return false, 0 } return true, int((lockoutDuration - elapsed).Seconds()) } // Fail 记录一次失败,达到阈值则锁定 func (l *loginLimiter) Fail(name string) { l.mu.Lock() defer l.mu.Unlock() now := time.Now() r, ok := l.recs[name] if !ok || now.Sub(r.firstSeen) > failureWindow { l.recs[name] = &failureRecord{count: 1, firstSeen: now} return } r.count++ if r.count >= maxLoginFailures { r.lockedAt = now } } // Reset 登录成功后清除失败计数 func (l *loginLimiter) Reset(name string) { l.mu.Lock() defer l.mu.Unlock() delete(l.recs, name) } // 定期清理过期记录,避免 map 无限增长 func init() { go func() { t := time.NewTicker(10 * time.Minute) defer t.Stop() for range t.C { limiter.mu.Lock() now := time.Now() for k, r := range limiter.recs { stale := now.Sub(r.firstSeen) > failureWindow && (r.lockedAt.IsZero() || now.Sub(r.lockedAt) > lockoutDuration) if stale { delete(limiter.recs, k) } } limiter.mu.Unlock() } }() }