| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- package middleware
- import (
- "context"
- "fmt"
- "log"
- "net/http"
- "strconv"
- "time"
- ippkg "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
- "github.com/gin-gonic/gin"
- "github.com/redis/go-redis/v9"
- )
- // RateLimitFailureMode Redis 故障策略
- type RateLimitFailureMode int
- const (
- RateLimitFailOpen RateLimitFailureMode = iota
- RateLimitFailClose
- )
- // RateLimitOptions 限流可选配置
- type RateLimitOptions struct {
- FailureMode RateLimitFailureMode
- }
- var rateLimitScript = redis.NewScript(`
- local current = redis.call('INCR', KEYS[1])
- local ttl = redis.call('PTTL', KEYS[1])
- local repaired = 0
- if current == 1 then
- redis.call('PEXPIRE', KEYS[1], ARGV[1])
- elseif ttl == -1 then
- redis.call('PEXPIRE', KEYS[1], ARGV[1])
- repaired = 1
- end
- return {current, repaired}
- `)
- // rateLimitRun 允许测试覆写脚本执行逻辑
- var rateLimitRun = func(ctx context.Context, client *redis.Client, key string, windowMillis int64) (int64, bool, error) {
- values, err := rateLimitScript.Run(ctx, client, []string{key}, windowMillis).Slice()
- if err != nil {
- return 0, false, err
- }
- if len(values) < 2 {
- return 0, false, fmt.Errorf("rate limit script returned %d values", len(values))
- }
- count, err := parseInt64(values[0])
- if err != nil {
- return 0, false, err
- }
- repaired, err := parseInt64(values[1])
- if err != nil {
- return 0, false, err
- }
- return count, repaired == 1, nil
- }
- // RateLimiter Redis 速率限制器
- type RateLimiter struct {
- redis *redis.Client
- prefix string
- }
- // NewRateLimiter 创建速率限制器实例
- func NewRateLimiter(redisClient *redis.Client) *RateLimiter {
- return &RateLimiter{
- redis: redisClient,
- prefix: "rate_limit:",
- }
- }
- // AllowResult 单次固定窗口限流判定结果。
- type AllowResult struct {
- // Allowed 是否放行
- Allowed bool
- // Count 当前窗口内累计请求数(含本次)
- Count int64
- // RetryAfter 超限时距窗口重置的剩余时间(尽力而为;PTTL 不可用时回退为完整窗口)
- RetryAfter time.Duration
- }
- // Allow 对给定 key(不含 "rate_limit:" 前缀)执行一次固定窗口计数判定。
- // 供需要自定义限流维度(如按用户 ID)的调用方使用;Redis 错误由调用方决定 fail-open/close。
- func (r *RateLimiter) Allow(ctx context.Context, key string, limit int, window time.Duration) (AllowResult, error) {
- redisKey := r.prefix + key
- windowMillis := windowTTLMillis(window)
- count, repaired, err := rateLimitRun(ctx, r.redis, redisKey, windowMillis)
- if err != nil {
- return AllowResult{}, err
- }
- if repaired {
- log.Printf("[RateLimit] ttl repaired: key=%s window_ms=%d", redisKey, windowMillis)
- }
- result := AllowResult{Allowed: count <= int64(limit), Count: count}
- if !result.Allowed {
- result.RetryAfter = window
- if ttl, ttlErr := r.redis.PTTL(ctx, redisKey).Result(); ttlErr == nil && ttl > 0 {
- result.RetryAfter = ttl
- }
- }
- return result, nil
- }
- // clientIPForRateLimit 返回 IP 维度限流使用的客户端地址。
- // 与审计日志/会话绑定/API Key IP ACL 共用同一套安全客户端 IP 解析
- // (SessionBindingContext 快照:兼容开关开启时信任反代转发头,关闭时走
- // server.trusted_proxies 可信链)。避免默认反代部署下 Gin ClientIP 恒等于
- // 代理地址、所有用户坍缩进同一个限流桶造成整体误拦截。
- func clientIPForRateLimit(c *gin.Context) string {
- if resolved := ippkg.GetSecurityClientIP(c, false); resolved != "" {
- return resolved
- }
- return c.ClientIP()
- }
- // Limit 返回速率限制中间件
- // key: 限制类型标识
- // limit: 时间窗口内最大请求数
- // window: 时间窗口
- func (r *RateLimiter) Limit(key string, limit int, window time.Duration) gin.HandlerFunc {
- return r.LimitWithOptions(key, limit, window, RateLimitOptions{})
- }
- // LimitWithOptions 返回速率限制中间件(带可选配置)
- func (r *RateLimiter) LimitWithOptions(key string, limit int, window time.Duration, opts RateLimitOptions) gin.HandlerFunc {
- failureMode := opts.FailureMode
- if failureMode != RateLimitFailClose {
- failureMode = RateLimitFailOpen
- }
- return func(c *gin.Context) {
- result, err := r.Allow(c.Request.Context(), key+":"+clientIPForRateLimit(c), limit, window)
- if err != nil {
- log.Printf("[RateLimit] redis error: key=%s mode=%s err=%v", r.prefix+key, failureModeLabel(failureMode), err)
- if failureMode == RateLimitFailClose {
- abortRateLimit(c, window)
- return
- }
- // Redis 错误时放行,避免影响正常服务
- c.Next()
- return
- }
- // 超过限制
- if !result.Allowed {
- abortRateLimit(c, result.RetryAfter)
- return
- }
- c.Next()
- }
- }
- func windowTTLMillis(window time.Duration) int64 {
- ttl := window.Milliseconds()
- if ttl < 1 {
- return 1
- }
- return ttl
- }
- func abortRateLimit(c *gin.Context, retryAfter time.Duration) {
- if retryAfter > 0 {
- seconds := int64(retryAfter / time.Second)
- if retryAfter%time.Second > 0 {
- seconds++
- }
- c.Header("Retry-After", strconv.FormatInt(seconds, 10))
- }
- c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
- "error": "rate limit exceeded",
- "message": "Too many requests, please try again later",
- })
- }
- func failureModeLabel(mode RateLimitFailureMode) string {
- if mode == RateLimitFailClose {
- return "fail-close"
- }
- return "fail-open"
- }
- func parseInt64(value any) (int64, error) {
- switch v := value.(type) {
- case int64:
- return v, nil
- case int:
- return int64(v), nil
- case string:
- parsed, err := strconv.ParseInt(v, 10, 64)
- if err != nil {
- return 0, err
- }
- return parsed, nil
- default:
- return 0, fmt.Errorf("unexpected value type %T", value)
- }
- }
|