60 lines
1.7 KiB
Go
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))
|
|
}
|