feat: add http rate limiting
This commit is contained in:
@@ -38,6 +38,13 @@ Port = 8080 # HTTP 监听端口
|
|||||||
# AllowMethods = "GET,POST,PUT,DELETE"
|
# AllowMethods = "GET,POST,PUT,DELETE"
|
||||||
# ExposeHeaders = "X-Request-Id"
|
# ExposeHeaders = "X-Request-Id"
|
||||||
# AllowCredentials = true
|
# AllowCredentials = true
|
||||||
|
|
||||||
|
[Http.RateLimit]
|
||||||
|
# Enabled = true
|
||||||
|
# Max = 120 # 每窗口最大请求数
|
||||||
|
# WindowSeconds = 60 # 窗口大小(秒)
|
||||||
|
# Message = "Too Many Requests"
|
||||||
|
# SkipPaths = ["/healthz", "/readyz"]
|
||||||
# =========================
|
# =========================
|
||||||
# Connection Multiplexer (providers/cmux)
|
# Connection Multiplexer (providers/cmux)
|
||||||
# 用于同端口同时暴露 HTTP + gRPC:cmux -> 分发到 Http/Grpc
|
# 用于同端口同时暴露 HTTP + gRPC:cmux -> 分发到 Http/Grpc
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Config struct {
|
|||||||
BaseURI *string
|
BaseURI *string
|
||||||
Tls *Tls
|
Tls *Tls
|
||||||
Cors *Cors
|
Cors *Cors
|
||||||
|
RateLimit *RateLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tls struct {
|
type Tls struct {
|
||||||
@@ -27,6 +28,14 @@ type Cors struct {
|
|||||||
Whitelist []Whitelist
|
Whitelist []Whitelist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RateLimit struct {
|
||||||
|
Enabled bool
|
||||||
|
Max int
|
||||||
|
WindowSeconds int
|
||||||
|
Message string
|
||||||
|
SkipPaths []string
|
||||||
|
}
|
||||||
|
|
||||||
type Whitelist struct {
|
type Whitelist struct {
|
||||||
AllowOrigin string
|
AllowOrigin string
|
||||||
AllowHeaders string
|
AllowHeaders string
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -16,10 +17,13 @@ import (
|
|||||||
"github.com/gofiber/fiber/v3/middleware/compress"
|
"github.com/gofiber/fiber/v3/middleware/compress"
|
||||||
"github.com/gofiber/fiber/v3/middleware/cors"
|
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||||
"github.com/gofiber/fiber/v3/middleware/helmet"
|
"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/logger"
|
||||||
"github.com/gofiber/fiber/v3/middleware/recover"
|
"github.com/gofiber/fiber/v3/middleware/recover"
|
||||||
"github.com/gofiber/fiber/v3/middleware/requestid"
|
"github.com/gofiber/fiber/v3/middleware/requestid"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"quyun/v2/app/errorx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultProvider() container.ProviderContainer {
|
func DefaultProvider() container.ProviderContainer {
|
||||||
@@ -137,8 +141,51 @@ func Provide(opts ...opt.Option) error {
|
|||||||
TimeZone: "Asia/Shanghai",
|
TimeZone: "Asia/Shanghai",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// rate limit (enable standard headers; adjust Max via config if needed)
|
// rate limit (by tenant code or IP)
|
||||||
// engine.Use(limiter.New(limiter.Config{Max: 0}))
|
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)
|
// static files (Fiber v3 Static helper moved; enable via filesystem middleware later)
|
||||||
// if config.StaticRoute != nil && config.StaticPath != nil { ... }
|
// if config.StaticRoute != nil && config.StaticPath != nil { ... }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- 认证仅使用 JWT(不做 OAuth/Cookie 方案)。
|
- 认证仅使用 JWT(不做 OAuth/Cookie 方案)。
|
||||||
- 支付集成暂不做,订单/退款仅按既有数据结构做流程与统计。
|
- 支付集成暂不做,订单/退款仅按既有数据结构做流程与统计。
|
||||||
- 存储仅使用本地 FS 模拟,真实 Provider 延后统一接入。
|
- 存储仅使用本地 FS 模拟,真实 Provider 延后统一接入。
|
||||||
- 多租户路由强隔离(`/t/:tenantCode/v1` + TenantResolver)暂缓,后续统一优化。
|
- 多租户路由强隔离(`/t/:tenantCode/v1` + TenantResolver)已启用,后续仅做细节优化。
|
||||||
|
|
||||||
## 统一原则
|
## 统一原则
|
||||||
- 所有后端改动遵循 `backend/llm.txt` 规范与 GORM-Gen 访问方式。
|
- 所有后端改动遵循 `backend/llm.txt` 规范与 GORM-Gen 访问方式。
|
||||||
@@ -157,18 +157,7 @@
|
|||||||
|
|
||||||
## P3(延后)
|
## P3(延后)
|
||||||
|
|
||||||
### 10) 多租户强隔离(路由 + TenantResolver)
|
### 10) 真实存储 Provider 接入
|
||||||
**需求目标**
|
|
||||||
- `/t/:tenantCode/v1` 作为唯一入口,服务层强制 tenant_id 过滤。
|
|
||||||
|
|
||||||
**技术方案(后端/前端)**
|
|
||||||
- 中间件解析 tenant_code → tenant_id 并注入 ctx locals。
|
|
||||||
- 前端 Router/API base 从 URL 解析 tenantCode。
|
|
||||||
|
|
||||||
**测试方案**
|
|
||||||
- 跨租户访问被拒绝;错租户路由返回 404。
|
|
||||||
|
|
||||||
### 11) 真实存储 Provider 接入
|
|
||||||
**需求目标**
|
**需求目标**
|
||||||
- 接入 OSS/云存储,统一上传/访问路径策略。
|
- 接入 OSS/云存储,统一上传/访问路径策略。
|
||||||
|
|
||||||
@@ -178,7 +167,7 @@
|
|||||||
**测试方案**
|
**测试方案**
|
||||||
- 本地 FS + 真实 Provider 两套配置可用性。
|
- 本地 FS + 真实 Provider 两套配置可用性。
|
||||||
|
|
||||||
### 12) 支付集成
|
### 11) 支付集成
|
||||||
**需求目标**
|
**需求目标**
|
||||||
- 最终阶段对接真实支付。
|
- 最终阶段对接真实支付。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user