You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
47 lines
1.2 KiB
47 lines
1.2 KiB
package middleware
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"laic-backend/common"
|
|
"laic-backend/logger"
|
|
)
|
|
|
|
// rateLimitLua 固定窗口计数:首次计数设置过期时间,返回当前窗口计数
|
|
const rateLimitLua = `
|
|
local key = KEYS[1]
|
|
local ttl = tonumber(ARGV[1])
|
|
local n = redis.call('INCR', key)
|
|
if n == 1 then
|
|
redis.call('EXPIRE', key, ttl)
|
|
end
|
|
return n
|
|
`
|
|
|
|
// RateLimitMiddleware 基于 Redis 固定窗口的按 IP 限流(默认 300 次/分钟)
|
|
func RateLimitMiddleware(limit int64, windowSeconds int64) gin.HandlerFunc {
|
|
if limit <= 0 {
|
|
limit = 300
|
|
}
|
|
if windowSeconds <= 0 {
|
|
windowSeconds = 60
|
|
}
|
|
return func(c *gin.Context) {
|
|
key := fmt.Sprintf("laic:ratelimit:%s", c.ClientIP())
|
|
n, err := common.GetLuaInt64(rateLimitLua, []string{key}, windowSeconds)
|
|
if err != nil {
|
|
// 限流依赖 Redis,异常时放行(避免 Redis 故障拖垮全站)
|
|
logger.ERROR("限流计数失败", err)
|
|
c.Next()
|
|
return
|
|
}
|
|
if n > limit {
|
|
common.FailWithBusiErrorWithHttpStatus(c, http.StatusTooManyRequests, common.NewBusiError(http.StatusTooManyRequests, "请求过于频繁,请稍后再试"))
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|