feat: add http rate limiting
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 { ... }
|
||||
|
||||
@@ -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) 支付集成
|
||||
**需求目标**
|
||||
- 最终阶段对接真实支付。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user