package middleware import ( "net/http" "strings" "sync" "time" "github.com/gin-gonic/gin" ) // loginAttempt 登录失败计数(按 IP + 用户名维度) type loginAttempt struct { failures int lockUntil time.Time lastFail time.Time } type loginLimiter struct { mu sync.Mutex seen map[string]*loginAttempt } const ( maxFailures = 5 // 连续失败 5 次后锁定 lockDuration = 15 * time.Minute failureWindow = 10 * time.Minute // 失败计数窗口 cleanupInterval = 5 * time.Minute ) var defaultLoginLimiter = newLoginLimiter() func newLoginLimiter() *loginLimiter { l := &loginLimiter{seen: make(map[string]*loginAttempt)} go l.cleanupLoop() return l } func (l *loginLimiter) cleanupLoop() { t := time.NewTicker(cleanupInterval) defer t.Stop() for range t.C { l.mu.Lock() now := time.Now() for k, v := range l.seen { if now.After(v.lockUntil) && now.Sub(v.lastFail) > failureWindow { delete(l.seen, k) } } l.mu.Unlock() } } // key = ip + "|" + username(小写) func limiterKey(c *gin.Context, username string) string { return c.ClientIP() + "|" + strings.ToLower(strings.TrimSpace(username)) } // checkLock 返回是否被锁定及剩余锁定时间 func (l *loginLimiter) checkLock(key string) (bool, time.Duration) { l.mu.Lock() defer l.mu.Unlock() a, ok := l.seen[key] if !ok { return false, 0 } if time.Now().Before(a.lockUntil) { return true, time.Until(a.lockUntil) } return false, 0 } // recordFailure 记录一次失败,达到阈值则锁定 func (l *loginLimiter) recordFailure(key string) { l.mu.Lock() defer l.mu.Unlock() a, ok := l.seen[key] if !ok { a = &loginAttempt{} l.seen[key] = a } now := time.Now() // 窗口外重置 if now.Sub(a.lastFail) > failureWindow { a.failures = 0 } a.failures++ a.lastFail = now if a.failures >= maxFailures { a.lockUntil = now.Add(lockDuration) } } // recordSuccess 登录成功后清空计数 func (l *loginLimiter) recordSuccess(key string) { l.mu.Lock() delete(l.seen, key) l.mu.Unlock() } // CheckLoginLock 检查是否被锁定,被锁定则写 429 并返回 true(在 handler 解析 body 后调用) func CheckLoginLock(c *gin.Context, username string) bool { key := limiterKey(c, username) if locked, remain := defaultLoginLimiter.checkLock(key); locked { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ "error": "登录尝试过多,已锁定,请稍后再试", "retry": int(remain.Minutes()) + 1, }) return true } return false } // RecordLoginFail 记录登录失败 func RecordLoginFail(c *gin.Context, username string) { defaultLoginLimiter.recordFailure(limiterKey(c, username)) } // RecordLoginSuccess 登录成功后清空计数 func RecordLoginSuccess(c *gin.Context, username string) { defaultLoginLimiter.recordSuccess(limiterKey(c, username)) }