From c399a65d833326e8a3e8d9cd5a8247358689c0e7 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sat, 17 Jan 2026 09:47:49 +0800 Subject: [PATCH] feat: add http rate limiting --- backend/config.full.toml | 7 +++++ backend/providers/http/config.go | 9 ++++++ backend/providers/http/engine.go | 51 ++++++++++++++++++++++++++++++-- docs/todo_list.md | 17 ++--------- 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/backend/config.full.toml b/backend/config.full.toml index 0ea2865..a782cdc 100644 --- a/backend/config.full.toml +++ b/backend/config.full.toml @@ -38,6 +38,13 @@ Port = 8080 # HTTP 监听端口 # AllowMethods = "GET,POST,PUT,DELETE" # ExposeHeaders = "X-Request-Id" # AllowCredentials = true + +[Http.RateLimit] +# Enabled = true +# Max = 120 # 每窗口最大请求数 +# WindowSeconds = 60 # 窗口大小(秒) +# Message = "Too Many Requests" +# SkipPaths = ["/healthz", "/readyz"] # ========================= # Connection Multiplexer (providers/cmux) # 用于同端口同时暴露 HTTP + gRPC:cmux -> 分发到 Http/Grpc diff --git a/backend/providers/http/config.go b/backend/providers/http/config.go index 5a7b9a9..8a91ed1 100644 --- a/backend/providers/http/config.go +++ b/backend/providers/http/config.go @@ -15,6 +15,7 @@ type Config struct { BaseURI *string Tls *Tls Cors *Cors + RateLimit *RateLimit } type Tls struct { @@ -27,6 +28,14 @@ type Cors struct { Whitelist []Whitelist } +type RateLimit struct { + Enabled bool + Max int + WindowSeconds int + Message string + SkipPaths []string +} + type Whitelist struct { AllowOrigin string AllowHeaders string diff --git a/backend/providers/http/engine.go b/backend/providers/http/engine.go index 57a10ab..9453451 100644 --- a/backend/providers/http/engine.go +++ b/backend/providers/http/engine.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "runtime/debug" + "strings" "time" log "github.com/sirupsen/logrus" @@ -16,10 +17,13 @@ import ( "github.com/gofiber/fiber/v3/middleware/compress" "github.com/gofiber/fiber/v3/middleware/cors" "github.com/gofiber/fiber/v3/middleware/helmet" + "github.com/gofiber/fiber/v3/middleware/limiter" "github.com/gofiber/fiber/v3/middleware/logger" "github.com/gofiber/fiber/v3/middleware/recover" "github.com/gofiber/fiber/v3/middleware/requestid" "github.com/samber/lo" + + "quyun/v2/app/errorx" ) func DefaultProvider() container.ProviderContainer { @@ -137,8 +141,51 @@ func Provide(opts ...opt.Option) error { TimeZone: "Asia/Shanghai", })) - // rate limit (enable standard headers; adjust Max via config if needed) - // engine.Use(limiter.New(limiter.Config{Max: 0})) + // rate limit (by tenant code or IP) + if config.RateLimit != nil && config.RateLimit.Enabled { + max := config.RateLimit.Max + if max <= 0 { + max = 120 + } + windowSeconds := config.RateLimit.WindowSeconds + if windowSeconds <= 0 { + windowSeconds = 60 + } + message := strings.TrimSpace(config.RateLimit.Message) + + skipPrefixes := append([]string{"/healthz", "/readyz"}, config.RateLimit.SkipPaths...) + engine.Use(limiter.New(limiter.Config{ + Max: max, + Expiration: time.Duration(windowSeconds) * time.Second, + LimitReached: func(c fiber.Ctx) error { + appErr := errorx.ErrRateLimitExceeded + if message != "" { + appErr = appErr.WithMsg(message) + } + return errorx.SendError(c, appErr) + }, + Next: func(c fiber.Ctx) bool { + path := c.Path() + for _, prefix := range skipPrefixes { + if prefix == "" { + continue + } + if strings.HasPrefix(path, prefix) { + return true + } + } + return false + }, + KeyGenerator: func(c fiber.Ctx) string { + if strings.HasPrefix(c.Path(), "/t/") { + if tenantCode := strings.TrimSpace(c.Params("tenantCode")); tenantCode != "" { + return "tenant:" + tenantCode + } + } + return c.IP() + }, + })) + } // static files (Fiber v3 Static helper moved; enable via filesystem middleware later) // if config.StaticRoute != nil && config.StaticPath != nil { ... } diff --git a/docs/todo_list.md b/docs/todo_list.md index 83cf70c..e1929df 100644 --- a/docs/todo_list.md +++ b/docs/todo_list.md @@ -6,7 +6,7 @@ - 认证仅使用 JWT(不做 OAuth/Cookie 方案)。 - 支付集成暂不做,订单/退款仅按既有数据结构做流程与统计。 - 存储仅使用本地 FS 模拟,真实 Provider 延后统一接入。 -- 多租户路由强隔离(`/t/:tenantCode/v1` + TenantResolver)暂缓,后续统一优化。 +- 多租户路由强隔离(`/t/:tenantCode/v1` + TenantResolver)已启用,后续仅做细节优化。 ## 统一原则 - 所有后端改动遵循 `backend/llm.txt` 规范与 GORM-Gen 访问方式。 @@ -157,18 +157,7 @@ ## P3(延后) -### 10) 多租户强隔离(路由 + TenantResolver) -**需求目标** -- `/t/:tenantCode/v1` 作为唯一入口,服务层强制 tenant_id 过滤。 - -**技术方案(后端/前端)** -- 中间件解析 tenant_code → tenant_id 并注入 ctx locals。 -- 前端 Router/API base 从 URL 解析 tenantCode。 - -**测试方案** -- 跨租户访问被拒绝;错租户路由返回 404。 - -### 11) 真实存储 Provider 接入 +### 10) 真实存储 Provider 接入 **需求目标** - 接入 OSS/云存储,统一上传/访问路径策略。 @@ -178,7 +167,7 @@ **测试方案** - 本地 FS + 真实 Provider 两套配置可用性。 -### 12) 支付集成 +### 11) 支付集成 **需求目标** - 最终阶段对接真实支付。