package middleware import ( "context" "net/http" "sync" "time" "github.com/gin-gonic/gin" "github.com/sundynix/sundynix-gateway/internal/store" ) // 限流的 fail-safe 兜底。Redis 是限流的主后端,但它一挂(或降级)时,此前 `Allow` 直接放行 // = fail-open:爆破/洪泛防护随 Redis 一起消失。这里加一个**进程内**固定窗口兜底限流,让 // Redis 不可用时仍有每实例的宽松限流(fail-safe),不牺牲整体可用性(本地限流很快、无外部依赖)。 // procLimiter 是进程内固定窗口计数器(每实例独立,不追求精确——兜底而已)。 type procLimiter struct { mu sync.Mutex counts map[string]*winCount window time.Duration } type winCount struct { n int reset time.Time } func newProcLimiter(window time.Duration) *procLimiter { return &procLimiter{counts: make(map[string]*winCount), window: window} } // allow 固定窗口内对 key 累加,超 limit 拒绝。顺带惰性清理过期项防 map 无界增长。 func (l *procLimiter) allow(key string, limit int, now time.Time) bool { l.mu.Lock() defer l.mu.Unlock() if len(l.counts) > 10000 { for k, wc := range l.counts { if now.After(wc.reset) { delete(l.counts, k) } } } wc := l.counts[key] if wc == nil || now.After(wc.reset) { l.counts[key] = &winCount{n: 1, reset: now.Add(l.window)} return true } wc.n++ return wc.n <= limit } // 全局兜底器(1 分钟窗口,与 Redis 限流同窗口口径)。 var fallbackLimiter = newProcLimiter(time.Minute) // allowWithFallback 优先用 Redis 限流;Redis 降级/故障(!Enabled 或 Allow 出错)时回落进程内兜底。 // 返回 true=放行。这是把 fail-open 改成 fail-safe 的关键接缝。 func allowWithFallback(cache *store.Redis, ctx context.Context, key string, limit int) bool { ok, err := cache.Allow(ctx, key, int64(limit), time.Minute) if !cache.Enabled() || err != nil { return fallbackLimiter.allow(key, limit, time.Now()) } return ok } // RateLimitN 对某类端点施加更严的独立限流(按 IP + prefix 分桶),用于登录/注册等爆破面大的 // 公开端点。与全局 RateLimit 叠加(两道桶都过才放行)。Redis 故障时走进程内兜底。 func RateLimitN(cache *store.Redis, perMinute int, prefix string) gin.HandlerFunc { return func(c *gin.Context) { key := prefix + ":" + c.ClientIP() if !allowWithFallback(cache, c.Request.Context(), key, perMinute) { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "请求过于频繁,请稍后再试"}) return } c.Next() } }