package middleware import ( "context" "errors" "fmt" "net/http" "sync" "sync/atomic" "time" "github.com/EthanCodeCraft/xlgo-core/database" "github.com/EthanCodeCraft/xlgo-core/response" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" ) // RateLimiter 速率限制器(内存版,单实例使用) type RateLimiter struct { visitors map[string]*visitor mu sync.RWMutex rate int // 每分钟允许的请求数 window time.Duration // 时间窗口 ctx context.Context cancel context.CancelFunc wg sync.WaitGroup nowFunc func() time.Time // 时间源(默认 time.Now,测试可注入可控时钟) } type visitor struct { // windowStart 当前固定窗口的起点。仅在新窗口开始时设置,放行时不变更(H4a 修复)。 // 旧实现每次放行都更新 lastSeen,导致 time.Since(lastSeen) > window 重置分支对持续 // 客户端永不成立、count 单调累加,稳态客户端(低于 rate)被误限流。 windowStart time.Time count int } // NewRateLimiter 创建速率限制器(内存版)。 // rate<=0 或 window<=0 时 panic。内部启动 cleanup goroutine,调用方负责在不再使用时 // 调用 (*RateLimiter).Stop 释放,否则 goroutine 泄漏。 func NewRateLimiter(rate int, window time.Duration) *RateLimiter { mustValidRateLimit(rate, window) ctx, cancel := context.WithCancel(context.Background()) limiter := &RateLimiter{ visitors: make(map[string]*visitor), rate: rate, window: window, ctx: ctx, cancel: cancel, nowFunc: time.Now, } limiter.wg.Add(1) go limiter.cleanupVisitors() return limiter } func mustValidRateLimit(rate int, window time.Duration) { if rate <= 0 { panic("rate limiter: rate must be positive") } if window <= 0 { panic("rate limiter: window must be positive") } } // now 返回当前时间(可被测试注入 nowFunc 覆盖)。 func (rl *RateLimiter) now() time.Time { if rl.nowFunc != nil { return rl.nowFunc() } return time.Now() } // SetNowFunc 注入时间源(默认 time.Now),供测试用可控时钟验证窗口语义。 // 生产代码通常无需调用。 func (rl *RateLimiter) SetNowFunc(f func() time.Time) { rl.mu.Lock() rl.nowFunc = f rl.mu.Unlock() } // Allow 检查是否允许请求。 // // 固定窗口语义(H4a 修复):windowStart 是当前窗口起点,仅在窗口过期重置时变更; // 放行时不再更新 windowStart。窗口内 count 达到 rate 即拒绝,窗口过期则重置为新窗口。 // 旧实现每次放行更新 lastSeen,致重置分支对持续客户端永不成立、稳态客户端被误限流。 // // 注意:固定窗口算法允许窗口边界突发(两窗口交界处瞬时可达 2×rate)。 // 如需平滑限流(无突发),请用 Redis 版 RedisRateLimiter(滑动窗口)。 func (rl *RateLimiter) Allow(ip string) bool { rl.mu.Lock() defer rl.mu.Unlock() now := rl.now() v, exists := rl.visitors[ip] if !exists { rl.visitors[ip] = &visitor{ windowStart: now, count: 1, } return true } // 窗口过期:开新窗口,count 重置为 1。 if now.Sub(v.windowStart) > rl.window { v.windowStart = now v.count = 1 return true } // 当前窗口内已达上限:拒绝(不更新 windowStart)。 if v.count >= rl.rate { return false } // 放行:count++,windowStart 不变(固定窗口语义)。 v.count++ return true } // cleanupVisitors 清理过期的访问者记录 func (rl *RateLimiter) cleanupVisitors() { defer rl.wg.Done() ticker := time.NewTicker(time.Minute) defer ticker.Stop() for { select { case <-rl.ctx.Done(): return case <-ticker.C: rl.mu.Lock() for ip, v := range rl.visitors { // 窗口起点超 window 未活跃即淘汰(H4a:windowStart 是窗口起点,不再被放行更新)。 if rl.now().Sub(v.windowStart) > rl.window { delete(rl.visitors, ip) } } rl.mu.Unlock() } } } // Stop 停止限流器(释放资源) func (rl *RateLimiter) Stop() { rl.cancel() rl.wg.Wait() } // ===== Redis 分布式限流器 ===== // H4c: Redis 限流器故障相关错误。 var ( // ErrRedisRateLimiterUnavailable Redis 未启用(database.GetRedis() == nil)。 // fail-closed 限流器在此情况下拒绝请求;fail-open 限流器放行。 ErrRedisRateLimiterUnavailable = errors.New("redis rate limiter: redis client unavailable") // ErrRedisRateLimiterUnexpectedResult Redis 返回非预期的结果类型(非 int64)。 // fail-closed 限流器拒绝;fail-open 限流器放行。旧实现裸断言会 panic。 ErrRedisRateLimiterUnexpectedResult = errors.New("redis rate limiter: unexpected result type") ) // RedisRateLimiter Redis 分布式限流器 type RedisRateLimiter struct { keyPrefix string // 键名前缀 rate int // 每分钟允许的请求数 window time.Duration // 时间窗口 failClosed atomic.Bool // H4c/H-6: Redis 错误/断言失败时是否拒绝(true=安全型 fail-closed)。atomic 以支持运行期 SetFailClosed 并发安全切换。 client *redis.Client // Phase 5: 注入的 redis client(nil 回退 database.GetRedis(),照 jwt.TokenBlacklist 模型)。App 经 WithRedisClient 注入 per-App client 实现隔离。 } // slidingWindowLua 滑动窗口限流 Lua 脚本 // 返回: 当前窗口内的请求数 const slidingWindowLua = ` local key = KEYS[1] local now = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local rate = tonumber(ARGV[3]) -- 移除窗口外的旧记录 redis.call('ZREMRANGEBYSCORE', key, 0, now - window) -- 获取当前窗口内的请求数 local count = redis.call('ZCARD', key) if count < rate then -- 添加当前请求 redis.call('ZADD', key, now, now .. '-' .. math.random()) redis.call('PEXPIRE', key, window) return 0 else return count end ` // RedisRateLimiterOption 配置 RedisRateLimiter 的可选策略。 type RedisRateLimiterOption func(*RedisRateLimiter) // WithFailClosed 设置 Redis 故障时 fail-closed(拒绝请求)。 // 不传或传 false 为 fail-open(放行,兼容默认)。安全敏感场景(登录防爆破等)应传 true。 func WithFailClosed(failClosed bool) RedisRateLimiterOption { return func(rl *RedisRateLimiter) { rl.failClosed.Store(failClosed) } } // WithRedisClient 注入指定 Redis 客户端(Phase 5,多 Redis/测试隔离)。 // 不传则回退 database.GetRedis()(全局)。App 可经 app.RedisClient() 取自身 client 注入。 func WithRedisClient(client *redis.Client) RedisRateLimiterOption { return func(rl *RedisRateLimiter) { rl.client = client } } // NewRedisRateLimiter 创建 Redis 分布式限流器。 // 默认 fail-open(Redis 故障时放行,避免影响业务);安全敏感场景传 WithFailClosed(true)。 func NewRedisRateLimiter(keyPrefix string, rate int, window time.Duration, opts ...RedisRateLimiterOption) *RedisRateLimiter { mustValidRateLimit(rate, window) rl := &RedisRateLimiter{ keyPrefix: keyPrefix, rate: rate, window: window, // failClosed 零值 false(fail-open,兼容默认) } for _, opt := range opts { if opt != nil { opt(rl) } } return rl } // SetFailClosed 设置 Redis 故障时的策略:true=拒绝(安全型),false=放行(兼容默认)。 // 供已创建的限流器切换策略。并发安全(atomic.Store),可与并发 Allow 调用共存。 func (rl *RedisRateLimiter) SetFailClosed(failClosed bool) { rl.failClosed.Store(failClosed) } // redisClient 返回注入的 redis client,未注入则回退 database.GetRedis()(jwt 模型)。 // M-C 教训:取一次复用,避免 nil 检查与 Eval 各调一次 GetRedis() 之间的 CloseRedis 竞态。 func (rl *RedisRateLimiter) redisClient() *redis.Client { if rl != nil && rl.client != nil { return rl.client } return database.GetRedis() } // Allow 检查是否允许请求。 // // H4c 修复: // - result.(int64) 改 comma-ok,断言失败返 ErrRedisRateLimiterUnexpectedResult 而非 panic。 // - Redis 错误/断言失败时按 failClosed 策略决定:fail-closed 返 (false, err) 拒绝, // fail-open 返 (true, err) 放行(兼容旧行为)。中间件层据此 allowed 值决定放行/拒绝, // 不再无条件 fail-open--登录防爆破等安全场景用 fail-closed 限流器即可在 Redis 故障时拒绝。 // // Redis 未启用(redisClient() == nil)时:fail-closed 返 (false, ErrRedisRateLimiterUnavailable), // fail-open 返 (true, nil)(兼容旧行为)。安全场景必须确保 Redis 已启用。 // // Phase 5:redisClient() 注入优先、全局兜底,App 经 WithRedisClient 注入 per-App client。 func (rl *RedisRateLimiter) Allow(ctx context.Context, identifier string) (bool, error) { // M-C 修复:取一次复用。原实现 nil 检查与 Eval 各调一次 GetRedis(), // 两次之间若 CloseRedis,第二次返回 nil -> nil.Eval panic。 rdb := rl.redisClient() if rdb == nil { if rl.failClosed.Load() { return false, ErrRedisRateLimiterUnavailable } return true, nil } key := rl.keyPrefix + ":" + identifier now := float64(time.Now().UnixMilli()) windowMs := float64(rl.window.Milliseconds()) result, err := rdb.Eval(ctx, slidingWindowLua, []string{key}, now, windowMs, rl.rate).Result() if err != nil { if rl.failClosed.Load() { return false, err } return true, err // 出错时允许请求,避免影响业务(兼容旧行为) } // H4c: comma-ok 断言,避免 Redis 返回非 int64 时 panic。 count, ok := result.(int64) if !ok { if rl.failClosed.Load() { return false, ErrRedisRateLimiterUnexpectedResult } return true, ErrRedisRateLimiterUnexpectedResult } return count == 0, nil } // GetCount 获取当前窗口内的请求数 func (rl *RedisRateLimiter) GetCount(ctx context.Context, identifier string) (int64, error) { // M-C 修复(Phase 5:改 redisClient):取一次复用(原三次调用存在 nil-deref 窗口)。 rdb := rl.redisClient() if rdb == nil { return 0, nil } key := rl.keyPrefix + ":" + identifier now := time.Now().UnixMilli() windowStart := now - rl.window.Milliseconds() // 移除旧记录并获取当前计数 rdb.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", windowStart)) return rdb.ZCard(ctx, key).Result() } // Reset 重置限流计数 func (rl *RedisRateLimiter) Reset(ctx context.Context, identifier string) error { // M-C 修复(Phase 5:改 redisClient):取一次复用(原两次调用存在 nil-deref 窗口)。 rdb := rl.redisClient() if rdb == nil { return nil } key := rl.keyPrefix + ":" + identifier return rdb.Del(ctx, key).Err() } // ===== 全局限速器 Registry(Phase 5 实例化) ===== // // 内存限流器(RateLimiter)持 cleanup goroutine,进程级共享会在多 App 下互相污染 // (App A Shutdown 经 StopRateLimiters 把 App B 的 loginLimiter 也停了)。故照 // cache.CacheManager / cron.Scheduler 模式引入 RateLimitRegistry:实例化 + 全局默认 // + 包级 facade 代理,App 持自己的 Registry。 // // 包级 LoginRateLimit() 等改为"请求时解析当前默认 Registry"懒创建限流器,使 App.Init // swap 后请求落到 App 的 Registry(避免 pre-Init 注册到 init Registry 被 swap 丢弃)。 // RateLimitRegistry 持一组内存限流器,支持 per-App 隔离。 type RateLimitRegistry struct { mu sync.Mutex loginLimiter *RateLimiter apiLimiter *RateLimiter uploadLimiter *RateLimiter customLimiters []*RateLimiter } // NewRateLimitRegistry 创建限流器注册表实例。 func NewRateLimitRegistry() *RateLimitRegistry { return &RateLimitRegistry{} } func (r *RateLimitRegistry) loginLimiterInstance() *RateLimiter { r.mu.Lock() defer r.mu.Unlock() if r.loginLimiter == nil { r.loginLimiter = NewRateLimiter(10, time.Minute) } return r.loginLimiter } func (r *RateLimitRegistry) apiLimiterInstance() *RateLimiter { r.mu.Lock() defer r.mu.Unlock() if r.apiLimiter == nil { r.apiLimiter = NewRateLimiter(100, time.Minute) } return r.apiLimiter } func (r *RateLimitRegistry) uploadLimiterInstance() *RateLimiter { r.mu.Lock() defer r.mu.Unlock() if r.uploadLimiter == nil { r.uploadLimiter = NewRateLimiter(20, time.Minute) } return r.uploadLimiter } // registerCustom 登记自定义限流器供 Stop 统一停止(H4b)。 func (r *RateLimitRegistry) registerCustom(l *RateLimiter) { r.mu.Lock() r.customLimiters = append(r.customLimiters, l) r.mu.Unlock() } // Init 预初始化标准限流器(可选;不调则首次请求时懒创建)。先停旧的同名限流器释放 goroutine。 func (r *RateLimitRegistry) Init() { r.mu.Lock() defer r.mu.Unlock() if r.loginLimiter != nil { r.loginLimiter.Stop() } if r.apiLimiter != nil { r.apiLimiter.Stop() } if r.uploadLimiter != nil { r.uploadLimiter.Stop() } r.loginLimiter = NewRateLimiter(10, time.Minute) r.apiLimiter = NewRateLimiter(100, time.Minute) r.uploadLimiter = NewRateLimiter(20, time.Minute) } // Stop 停止注册表中所有限流器(释放 cleanup goroutine)。幂等。 func (r *RateLimitRegistry) Stop() { r.mu.Lock() defer r.mu.Unlock() if r.loginLimiter != nil { r.loginLimiter.Stop() r.loginLimiter = nil } if r.apiLimiter != nil { r.apiLimiter.Stop() r.apiLimiter = nil } if r.uploadLimiter != nil { r.uploadLimiter.Stop() r.uploadLimiter = nil } for _, l := range r.customLimiters { l.Stop() } r.customLimiters = nil } // --- App-bound 中间件(捕获此 Registry,不查全局默认,多 App per-App 隔离) --- // // 与包级 LoginRateLimit() 等的区别:包级在请求时查 GetDefaultRateLimitRegistry()(多 App 下 // 仅最后 Init 的 App 是全局默认,故包级 facade 不提供 per-App 计数隔离);本组方法捕获 r, // 始终用此 Registry 的 limiter。多 App 场景用 app.RateLimitRegistry().LoginRateLimit() 装配路由。 // LoginRateLimit 返回绑定到此 Registry 的登录限流中间件。 func (r *RateLimitRegistry) LoginRateLimit() gin.HandlerFunc { return func(c *gin.Context) { rateLimitAllow(c, r.loginLimiterInstance()) } } // APIRateLimit 返回绑定到此 Registry 的普通 API 限流中间件。 func (r *RateLimitRegistry) APIRateLimit() gin.HandlerFunc { return func(c *gin.Context) { rateLimitAllow(c, r.apiLimiterInstance()) } } // UploadRateLimit 返回绑定到此 Registry 的上传限流中间件。 func (r *RateLimitRegistry) UploadRateLimit() gin.HandlerFunc { return func(c *gin.Context) { rateLimitAllow(c, r.uploadLimiterInstance()) } } // CustomRateLimit 返回绑定到此 Registry 的自定义限流中间件。limiter 在首次请求时创建并 // 登记到此 Registry(r.registerCustom),由 App.Shutdown 的 registry.Stop() 收口,无泄漏。 func (r *RateLimitRegistry) CustomRateLimit(rate int, window time.Duration) gin.HandlerFunc { var ( once sync.Once limiter *RateLimiter ) return func(c *gin.Context) { once.Do(func() { limiter = NewRateLimiter(rate, window) r.registerCustom(limiter) }) rateLimitAllow(c, limiter) } } // defaultRateLimitRegistry 全局默认 Registry(atomic,照 cache/cron 模式)。 var defaultRateLimitRegistry atomic.Pointer[RateLimitRegistry] func init() { defaultRateLimitRegistry.Store(NewRateLimitRegistry()) } // GetDefaultRateLimitRegistry 返回全局默认 Registry。 func GetDefaultRateLimitRegistry() *RateLimitRegistry { return defaultRateLimitRegistry.Load() } // SwapDefaultRateLimitRegistry 置为全局默认,返回被替换的旧(照 Swap 模式)。nil 忽略,返回当前默认。 func SwapDefaultRateLimitRegistry(r *RateLimitRegistry) *RateLimitRegistry { if r == nil { return defaultRateLimitRegistry.Load() } return defaultRateLimitRegistry.Swap(r) } // rateLimitAllow 公共放行/拒绝逻辑(内存版)。 func rateLimitAllow(c *gin.Context, rl *RateLimiter) { if rl == nil { response.Custom(c, http.StatusServiceUnavailable, response.CodeServiceUnavailable, "限流器未初始化", nil) c.Abort() return } if !rl.Allow(c.ClientIP()) { response.RateLimit(c) c.Abort() return } c.Next() } // RateLimit 通用速率限制中间件(内存版,用户自持 limiter 时使用)。 func RateLimit(limiter *RateLimiter) gin.HandlerFunc { return func(c *gin.Context) { rateLimitAllow(c, limiter) } } // LoginRateLimit 登录接口速率限制。限流器在首次请求时从当前默认 Registry 懒创建, // 故 App.Init swap 后用的是 App 的 Registry(避免 pre-Init 注册到 init Registry 被丢弃)。 func LoginRateLimit() gin.HandlerFunc { return func(c *gin.Context) { rateLimitAllow(c, GetDefaultRateLimitRegistry().loginLimiterInstance()) } } // APIRateLimit 普通 API 速率限制。 func APIRateLimit() gin.HandlerFunc { return func(c *gin.Context) { rateLimitAllow(c, GetDefaultRateLimitRegistry().apiLimiterInstance()) } } // UploadRateLimit 上传接口速率限制。 func UploadRateLimit() gin.HandlerFunc { return func(c *gin.Context) { rateLimitAllow(c, GetDefaultRateLimitRegistry().uploadLimiterInstance()) } } // CustomRateLimit 自定义速率限制(内存版)。限流器在首次请求时创建并登记到当前默认 // Registry(供 Stop 统一停止,避免 cleanup goroutine 泄漏,H4b)。 func CustomRateLimit(rate int, window time.Duration) gin.HandlerFunc { var ( once sync.Once limiter *RateLimiter ) return func(c *gin.Context) { once.Do(func() { limiter = NewRateLimiter(rate, window) GetDefaultRateLimitRegistry().registerCustom(limiter) }) rateLimitAllow(c, limiter) } } // InitRateLimiters 初始化默认 Registry 的标准限流器(可选,不调则懒创建)。 func InitRateLimiters() { GetDefaultRateLimitRegistry().Init() } // StopRateLimiters 停止默认 Registry 的所有限流器。注意:仅停默认 Registry-- // App 持自己的 Registry 时应在 Shutdown 调 app 自己的 Stop(Phase 5)。 func StopRateLimiters() { GetDefaultRateLimitRegistry().Stop() } // ===== Redis 分布式限流中间件 ===== // redisLimitDecision 处理 RedisRateLimiter.Allow 的结果,按 allowed 值决定放行/拒绝。 // // H4c: 不再无条件 fail-open。Allow 已按 limiter 的 failClosed 策略把 Redis 故障翻成 // allowed 值--fail-closed 时 allowed=false(拒绝),fail-open 时 allowed=true(放行)。 // 故 err 与 allowed 的组合语义: // - err==nil, allowed==true -> 放行 // - err==nil, allowed==false -> 真超限,返 429(response.RateLimit) // - err!=nil, allowed==false -> fail-closed 限流器在 Redis 故障下拒绝,返 503(服务不可用) // - err!=nil, allowed==true -> fail-open 限流器在 Redis 故障下放行(兼容旧行为) func redisLimitDecision(c *gin.Context, allowed bool, err error) { if err != nil && !allowed { // fail-closed:Redis 故障时拒绝(防限流静默失效)。返 503 区别于真实超限的 429。 response.Custom(c, http.StatusServiceUnavailable, response.CodeServiceUnavailable, "限流服务暂时不可用", nil) c.Abort() return } if !allowed { response.RateLimit(c) c.Abort() return } c.Next() } // RedisRateLimit Redis 分布式限流中间件。 // 默认 fail-open(Redis 故障时放行,避免影响业务);安全敏感场景(登录防爆破等) // 传 WithFailClosed(true),Redis 故障时拒绝以防限流失效。 func RedisRateLimit(keyPrefix string, rate int, opts ...RedisRateLimiterOption) gin.HandlerFunc { limiter := NewRedisRateLimiter(keyPrefix, rate, time.Minute, opts...) return func(c *gin.Context) { identifier := c.ClientIP() // 可选:使用用户ID作为标识(登录后) // userID := GetUserID(c) // if userID > 0 { // identifier = fmt.Sprintf("user:%d", userID) // } allowed, err := limiter.Allow(c.Request.Context(), identifier) redisLimitDecision(c, allowed, err) } } // RedisRateLimitWithIdentifier 自定义标识的 Redis 分布式限流。 // 参数: keyPrefix 键名前缀,rate 1 分钟窗口内允许的请求数,identifierFunc 标识获取函数。 // identifierFunc 为 nil 或返回空串时回退到 c.ClientIP()。默认 fail-open,传 WithFailClosed(true) 切换。 func RedisRateLimitWithIdentifier(keyPrefix string, rate int, identifierFunc func(c *gin.Context) string, opts ...RedisRateLimiterOption) gin.HandlerFunc { limiter := NewRedisRateLimiter(keyPrefix, rate, time.Minute, opts...) return func(c *gin.Context) { identifier := "" if identifierFunc != nil { identifier = identifierFunc(c) } if identifier == "" { identifier = c.ClientIP() } allowed, err := limiter.Allow(c.Request.Context(), identifier) redisLimitDecision(c, allowed, err) } } // LoginRedisRateLimit 登录接口 Redis 分布式限流(fail-closed)。 // // H4c: 登录防爆破场景必须 fail-closed--Redis 故障时若 fail-open 则限流失效、 // 攻击者可借 Redis 抖动窗口无限爆破。改为 fail-closed:Redis 故障时返 503 拒绝。 func LoginRedisRateLimit() gin.HandlerFunc { return RedisRateLimit("login_limit", 10, WithFailClosed(true)) } // APIRedisRateLimit API Redis 分布式限流(fail-open,避免影响业务)。 func APIRedisRateLimit() gin.HandlerFunc { return RedisRateLimit("api_limit", 100) } // UploadRedisRateLimit 上传接口 Redis 分布式限流(fail-closed)。 // 上传属资源敏感操作,Redis 故障时拒绝以防限流静默失效。 func UploadRedisRateLimit() gin.HandlerFunc { return RedisRateLimit("upload_limit", 20, WithFailClosed(true)) } // CustomRedisRateLimit 自定义 Redis 分布式限流。 // 默认 fail-open,传 WithFailClosed(true) 切换为 fail-closed。 func CustomRedisRateLimit(keyPrefix string, rate int, window time.Duration, opts ...RedisRateLimiterOption) gin.HandlerFunc { limiter := NewRedisRateLimiter(keyPrefix, rate, window, opts...) return func(c *gin.Context) { identifier := c.ClientIP() allowed, err := limiter.Allow(c.Request.Context(), identifier) redisLimitDecision(c, allowed, err) } }