package limitHandler import ( "errors" "fmt" "strconv" "sync" "time" "91porn-server/app/appg" "91porn-server/app/service/m3u8ticket" "91porn-server/common" "91porn-server/common/constant" "91porn-server/common/constant/redisconst" "91porn-server/common/log" "91porn-server/common/stderr" "91porn-server/models/v/usermod" "github.com/gin-gonic/gin" ) type RequestCountTime struct { //请求的词数 count int64 //最后的访问时间 lastTime int64 } // 限制用户的每秒的请求的次数,1s10次的频率 func FilterRequest(ctx *gin.Context) { uid, err := common.GetUID(ctx) if err != nil { common.ServeJSON(ctx, stderr.ErrNoToken, err) return } key := redisconst.RechargeLimtKey(uid) Expire := redisconst.RechargeLimtKeyExpire() if appg.Redis.IsExist(key) { common.ServeJSON(ctx, stderr.PayBusy, err) ctx.Abort() return } go func() { _ = appg.Redis.Set(key, "-", Expire) }() } // FilterRequestLimit 限制用户的每秒的请求的次数,1s1次的频率 func FilterRequestLimit(ctx *gin.Context) { uid, err := common.GetUID(ctx) if err != nil { common.ServeJSON(ctx, stderr.ErrNoToken, err) return } key := redisconst.ReqLimtKey(uid, ctx.Request.URL.Path) Expire := redisconst.ReqLimtKeyExpire() if appg.Redis.IsExist(key) { common.ServeJSON(ctx, stderr.PayBusy, err) ctx.Abort() return } go func() { _ = appg.Redis.Set(key, "-", Expire) }() } // FilterRequestByUser 限制用户短时间内的请求次数,limit决定限制时间 func FilterRequestByUser(keyFmt string, limit time.Duration, errCode stderr.Code) gin.HandlerFunc { return func(ctx *gin.Context) { uid, err := common.GetUID(ctx) if err != nil { common.ServeJSON(ctx, stderr.ErrNoToken, err) ctx.Abort() return } key := fmt.Sprintf(keyFmt, uid) if appg.Redis.IsExist(key) { common.ServeJSON(ctx, errCode, err) ctx.Abort() return } go func() { _ = appg.Redis.Set(key, "-", limit) }() } } // FilterSMSCaptchaByIP 限制单 IP 每自然日发送短信验证码的次数,超出 smsIPLimit 配置后拒绝 func FilterSMSCaptchaByIP(ctx *gin.Context) { if !appg.ShouldEnforceIPRateLimit() { return } ip := ctx.GetString(constant.CtxIP) if ip == "" { ip = ctx.ClientIP() } key := redisconst.SMSCaptchaIPKey(ip) cnt := appg.Redis.Incr(key) if cnt == 1 { _, _ = appg.Redis.ExpireKey(key, redisconst.SMSCaptchaIPExpire()) } limit := appg.Conf.Limit.SMSIPLimit if limit <= 0 { limit = 10 } if cnt > limit { log.Warn("sms captcha ip rate limit exceeded", log.Any("ip", ip)) common.ServeJSON(ctx, stderr.VisitLimit, nil) ctx.Abort() return } } // FilterByIP wraps an IP-based Gin limiter and bypasses it only in the test // environment. Other environments retain the original fail-safe behavior. func FilterByIP(handler gin.HandlerFunc) gin.HandlerFunc { return func(ctx *gin.Context) { if !appg.ShouldEnforceIPRateLimit() { return } handler(ctx) } } // FilterSMSCaptchaByUID 限制单用户发送短信验证码:1分钟内只能发1次,每自然日最多5次 func FilterSMSCaptchaByUID(ctx *gin.Context) { uid, err := common.GetUID(ctx) if err != nil { common.ServeJSON(ctx, stderr.ErrNoToken, err) ctx.Abort() return } // 1分钟冷却:key 存在则拒绝 cooldownKey := redisconst.SMSCaptchaUIDCooldownKey(uid) if appg.Redis.IsExist(cooldownKey) { log.Warn("sms captcha uid cooldown", log.Any("uid", uid)) common.ServeJSON(ctx, stderr.VisitLimit, nil) ctx.Abort() return } // 每日上限:计数器超过 5 次拒绝 dailyKey := redisconst.SMSCaptchaUIDKey(uid) cnt := appg.Redis.Incr(dailyKey) if cnt == 1 { _, _ = appg.Redis.ExpireKey(dailyKey, redisconst.SMSCaptchaIPExpire()) } if cnt > 5 { log.Warn("sms captcha uid daily limit exceeded", log.Any("uid", uid)) common.ServeJSON(ctx, stderr.VisitLimit, nil) ctx.Abort() return } // 通过后写入冷却 key,1分钟内不能再发 go func() { _ = appg.Redis.Set(cooldownKey, "-", redisconst.SMSCaptchaUIDCooldown) }() } func FilterRequestByUserAndTerminal(keyFmt string, limit time.Duration, errCode stderr.Code) gin.HandlerFunc { return func(ctx *gin.Context) { ua, err := common.GetUA(ctx) if err != nil { common.ServeJSON(ctx, stderr.BadUA, err) ctx.Abort() return } if ua.IsH5 == "1" { return } uid, err := common.GetUID(ctx) if err != nil { common.ServeJSON(ctx, stderr.ErrNoToken, err) ctx.Abort() return } key := fmt.Sprintf(keyFmt, uid) if appg.Redis.IsExist(key) { common.ServeJSON(ctx, errCode, err) ctx.Abort() return } go func() { _ = appg.Redis.Set(key, "-", limit) }() } } const ( limitCount = 7 limitExpire = 3 mapLength = 5000 ) var mu = sync.Mutex{} var limitMap = make(map[string]*RequestCountTime) func Limit(ctx *gin.Context) { uid, err := common.GetUID(ctx) if err != nil { common.ServeJSON(ctx, stderr.ErrNoToken, err) return } key := ctx.Request.RequestURI + strconv.FormatInt(int64(uid), 10) t1 := time.Now() mu.Lock() defer mu.Unlock() if len(limitMap) > mapLength { limitMap = make(map[string]*RequestCountTime) } v, ok := limitMap[key] if !ok { limitMap[key] = &RequestCountTime{ lastTime: t1.Unix(), } v = limitMap[key] } if t1.Unix() > v.lastTime+limitExpire { //超出计时周期 v.lastTime = t1.Unix() v.count = 0 } v.count++ if v.count > limitCount { log.Warn("abnormal user request too frequent", log.Any("uid", uid)) err = errors.New("user request too frequent") common.ServeJSON(ctx, stderr.ErrNoToken, err) return } } const ( m3u8HourlyFreeLimit = 1800 // 普通用户每小时 m3u8 请求上限 m3u8HourlyVipLimit = 3600 // 会员每小时 m3u8 请求上限 // 命中限流时下发的提示 m3u8(替换 *source,不报错):普通用户与会员分别下发不同提示片 m3u8LimitFallbackNormal = "/bktadminup/sp/zp/hb/7a/0q/860513c4947143b29083160ec41588b5.m3u8" // 普通用户 m3u8LimitFallbackVip = "/bktadminup/sp/26/gx/7n/cr/359754435033419a95036ab33e520719.m3u8" // 会员 ) // M3u8HourlyLimit 限制每个用户每小时请求 m3u8 的次数:普通用户 50 次,会员 200 次。 // 超过免费次数后仅会员可继续,且总量不超过会员上限。 func M3u8HourlyLimit(ctx *gin.Context) { uid, err := common.GetUID(ctx) if err != nil || uid == 0 { return // 取不到用户则不限流(交由上游鉴权处理) } key := redisconst.M3u8HourlyCountKey(uid) cnt := appg.Redis.Incr(key) if cnt == 1 { _, _ = appg.Redis.ExpireKey(key, redisconst.M3u8HourlyCountExpire) } if cnt <= m3u8HourlyFreeLimit { return } isVip := isM3u8VipUser(uid) // 命中限流:非会员超普通上限、或会员超会员上限。不报错,改写 *source 为对应提示 m3u8 交后续 handler 下发 if !isVip || cnt > m3u8HourlyVipLimit { log.Warn("m3u8 hourly rate limit exceeded", log.Any("uid", uid), log.Any("count", cnt), log.Any("vip", isVip)) fallback := m3u8LimitFallbackNormal if isVip { fallback = m3u8LimitFallbackVip } // 按最新规则(带票 + JHA 前缀)签发提示片地址,使严格验票路由(h5/m3u8)能识别并下发对应提示片, // 不被验票逻辑当作非法票据覆盖;未开启票据时 BuildSignedURL 返回空,退回明文兜底(此时下游也不验票)。 ua := "" if u, uaErr := common.GetUA(ctx); uaErr == nil { ua = u.UserAgent } if signed := m3u8ticket.BuildSignedURL(uid, fallback, common.GetIP(ctx), ua, true, false); signed != "" { fallback = signed } for i := range ctx.Params { if ctx.Params[i].Key == "source" { ctx.Params[i].Value = fallback } } return } } func isM3u8VipUser(uid uint64) bool { u, err := usermod.FindUserByUID(uid) if err != nil || u == nil { return false } return u.IsVIP(time.Now()) }