初始化项目:基础设施 + 用户认证 + 后台管理系统 + AGPL 3.0 许可
This commit is contained in:
149
internal/middleware/ratelimit.go
Normal file
149
internal/middleware/ratelimit.go
Normal file
@ -0,0 +1,149 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RateLimiter 双维度滑动窗口登录限流
|
||||
// 维度一(账户):同邮箱 1 分钟内失败 3 次 → 锁定 1 分钟
|
||||
// 维度二(IP):同 IP 1 分钟内失败 10 次 → 锁定 1 分钟
|
||||
type RateLimiter struct {
|
||||
mu sync.RWMutex
|
||||
// accountFailures key = 邮箱(小写)
|
||||
accountFailures map[string]*windowState
|
||||
// ipFailures key = IP
|
||||
ipFailures map[string]*windowState
|
||||
}
|
||||
|
||||
// windowState 单个维度的限流状态
|
||||
type windowState struct {
|
||||
count int
|
||||
windowStart time.Time
|
||||
blockedUntil time.Time // 零值为未封锁
|
||||
}
|
||||
|
||||
// RateLimitResult 限流检查结果
|
||||
type RateLimitResult struct {
|
||||
Blocked bool
|
||||
RetryAfter int // 剩余封锁秒数
|
||||
Message string
|
||||
}
|
||||
|
||||
const (
|
||||
accountWindow = 1 * time.Minute
|
||||
accountMaxFails = 3
|
||||
accountBlockDur = 1 * time.Minute
|
||||
ipWindow = 1 * time.Minute
|
||||
ipMaxFails = 10
|
||||
ipBlockDur = 1 * time.Minute
|
||||
cleanupInterval = 2 * time.Minute
|
||||
maxEntries = 10000 // 单维度最大条目数(防内存耗尽)
|
||||
)
|
||||
|
||||
// NewRateLimiter 创建限流器并启动后台清理
|
||||
func NewRateLimiter() *RateLimiter {
|
||||
rl := &RateLimiter{
|
||||
accountFailures: make(map[string]*windowState),
|
||||
ipFailures: make(map[string]*windowState),
|
||||
}
|
||||
go rl.cleanupLoop()
|
||||
return rl
|
||||
}
|
||||
|
||||
// AllowAccount 检查账户维度是否允许登录尝试
|
||||
// 返回 (result, 登录失败时应调用的记录函数)
|
||||
func (rl *RateLimiter) AllowAccount(email string) (RateLimitResult, func()) {
|
||||
return rl.check(rl.accountFailures, email, accountWindow, accountBlockDur, accountMaxFails, true)
|
||||
}
|
||||
|
||||
// AllowIP 检查 IP 维度是否允许登录尝试
|
||||
func (rl *RateLimiter) AllowIP(ip string) (RateLimitResult, func()) {
|
||||
return rl.check(rl.ipFailures, ip, ipWindow, ipBlockDur, ipMaxFails, false)
|
||||
}
|
||||
|
||||
// check 核心检查逻辑
|
||||
// lowKey: 是否需要脱敏日志(true = 暗示账户存在,仅泄漏给已知该邮箱的人)
|
||||
func (rl *RateLimiter) check(m map[string]*windowState, key string, window, blockDur time.Duration, maxFails int, lowKey bool) (RateLimitResult, func()) {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
state, exists := m[key]
|
||||
|
||||
if !exists {
|
||||
state = &windowState{}
|
||||
if len(m) < maxEntries {
|
||||
m[key] = state
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否处于封锁期
|
||||
if !state.blockedUntil.IsZero() && now.Before(state.blockedUntil) {
|
||||
retry := int(state.blockedUntil.Sub(now).Seconds()) + 1
|
||||
msg := "请求过于频繁,请稍后重试"
|
||||
if lowKey {
|
||||
msg = fmt.Sprintf("该账号登录尝试过于频繁,请 %d 秒后重试", retry)
|
||||
}
|
||||
return RateLimitResult{Blocked: true, RetryAfter: retry, Message: msg}, nil
|
||||
}
|
||||
|
||||
// 窗口过期 → 重置
|
||||
if now.Sub(state.windowStart) > window {
|
||||
state.count = 0
|
||||
state.windowStart = now
|
||||
state.blockedUntil = time.Time{}
|
||||
}
|
||||
|
||||
// 记录失败(由调用方在登录失败时调用)的闭包
|
||||
recordFail := func() {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
s := m[key]
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
if now.Sub(s.windowStart) > window {
|
||||
s.count = 1
|
||||
s.windowStart = now
|
||||
return
|
||||
}
|
||||
s.count++
|
||||
if s.count >= maxFails {
|
||||
s.blockedUntil = now.Add(blockDur)
|
||||
}
|
||||
}
|
||||
|
||||
return RateLimitResult{Blocked: false}, recordFail
|
||||
}
|
||||
|
||||
// Clear 登录成功后清除该 email 和 IP 的失败计数
|
||||
func (rl *RateLimiter) Clear(email, ip string) {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
delete(rl.accountFailures, email)
|
||||
delete(rl.ipFailures, ip)
|
||||
}
|
||||
|
||||
// cleanupLoop 定期清理过期条目
|
||||
func (rl *RateLimiter) cleanupLoop() {
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
rl.mu.Lock()
|
||||
now := time.Now()
|
||||
clean := func(m map[string]*windowState) {
|
||||
for k, v := range m {
|
||||
// 封锁期已过且窗口已过期 → 删除
|
||||
if (v.blockedUntil.IsZero() || now.After(v.blockedUntil)) &&
|
||||
now.Sub(v.windowStart) > accountWindow+ipBlockDur {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
clean(rl.accountFailures)
|
||||
clean(rl.ipFailures)
|
||||
rl.mu.Unlock()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user