Files
backend_v2/internal/middleware/ratelimit.go
toom1996 361ffc01af update
2026-09-29 19:59:30 +08:00

98 lines
2.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}