chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user