Files

60 lines
1.7 KiB
Go

package middleware
import (
"context"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
)
const (
maxFailures = 5 // 连续失败 5 次后锁定
lockDuration = 15 * time.Minute
failureWindow = 10 * time.Minute // 失败计数窗口
)
// key = ip + "|" + username(小写)
func limiterKey(c *gin.Context, username string) string {
return c.ClientIP() + "|" + strings.ToLower(strings.TrimSpace(username))
}
// CheckLoginLock 检查是否被锁定,被锁定则写 429 并返回 true(在 handler 解析 body 后调用)
func CheckLoginLock(c *gin.Context, username string) bool {
if authState == nil {
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": stateUnavailable("执行登录限流").Error()})
return true
}
key := limiterKey(c, username)
locked, remain, err := authState.CheckLoginLock(context.Background(), key)
if err != nil {
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": "登录状态服务不可用"})
return true
}
if 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) error {
if authState == nil {
return stateUnavailable("记录登录失败")
}
return authState.RecordLoginFailure(context.Background(), limiterKey(c, username))
}
// RecordLoginSuccess 登录成功后清空计数
func RecordLoginSuccess(c *gin.Context, username string) error {
if authState == nil {
return stateUnavailable("清空登录失败计数")
}
return authState.RecordLoginSuccess(context.Background(), limiterKey(c, username))
}