This commit is contained in:
toom1996
2026-09-29 19:59:30 +08:00
parent d15d2a4701
commit 361ffc01af
28 changed files with 896 additions and 47 deletions

View File

@ -0,0 +1,97 @@
package middleware
import (
"net/http"
"sync"
"time"
"fashionapi/internal/config"
"github.com/gin-gonic/gin"
)
// rateLimiter 基于令牌桶的按 IP 限流(进程内,单实例足够;多实例需换集中式如 redis)。
//
// 设计取舍:公开接口「首页公开、翻页需登录」已天然抬高批量枚举门槛;本中间件作为
// 兜底,按客户端 IP 限制总量,兜住 casual 爬虫与突发流量,避免单 IP 打垮服务。
type rateLimiter struct {
mu sync.Mutex
visitors map[string]*visitor
rate float64 // 每秒补充令牌数
burst float64 // 桶容量
lastSeen map[string]time.Time
}
type visitor struct {
tokens float64
last time.Time
}
func newRateLimiter(rps, burst int) *rateLimiter {
rl := &rateLimiter{
visitors: make(map[string]*visitor),
lastSeen: make(map[string]time.Time),
rate: float64(rps),
burst: float64(burst),
}
go rl.cleanup(10 * time.Minute)
return rl
}
// cleanup 周期清理长时间无活动的客户端,避免 map 无限增长。
func (rl *rateLimiter) cleanup(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
rl.mu.Lock()
for ip, t := range rl.lastSeen {
if time.Since(t) > 3*interval {
delete(rl.visitors, ip)
delete(rl.lastSeen, ip)
}
}
rl.mu.Unlock()
}
}
func (rl *rateLimiter) allow(ip string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
v, ok := rl.visitors[ip]
if !ok {
// 首次访问直接发放 burst-1 个令牌(允许一个突发请求),后续按速率补充
rl.visitors[ip] = &visitor{tokens: rl.burst - 1, last: now}
rl.lastSeen[ip] = now
return true
}
elapsed := now.Sub(v.last).Seconds()
v.tokens += elapsed * rl.rate
if v.tokens > rl.burst {
v.tokens = rl.burst
}
v.last = now
rl.lastSeen[ip] = now
if v.tokens >= 1 {
v.tokens--
return true
}
return false
}
// RateLimit 按 IP 的速率限制中间件。Enabled=false 时为 noop(便于灰度与本地开发)。
func RateLimit(cfg config.RateLimitConfig) gin.HandlerFunc {
if !cfg.Enabled || cfg.RPS <= 0 {
return func(c *gin.Context) { c.Next() }
}
rl := newRateLimiter(cfg.RPS, cfg.Burst)
return func(c *gin.Context) {
if !rl.allow(c.ClientIP()) {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "too many requests",
})
return
}
c.Next()
}
}