mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-06 08:49:24 +00:00
用户可高频刷面板接口(usage/dashboard 等重聚合查询)直接打爆数据库:
现有限流器只覆盖登录/注册等公开认证入口,登录后的全部面板端点无任何限流。
三层防护(阈值均可在后台可视化配置,panel_rate_limit_settings):
1. 认证面板接口按「用户 ID」限流,与来源 IP 无关——反向代理/NAT 共享出口
(所有请求源地址坍缩为 127.0.0.1 等)不会互相误伤:
- Global 档(默认 240 rpm/账号):user/auth/payment/admin 全部登录后路由
- Heavy 档(默认 60 rpm/账号):/usage、/usage/dashboard/*、
/user/api-keys/:id/usage/daily 等重 SQL 聚合端点叠加计数
- 管理员默认豁免(可关闭)
2. 无认证公开接口(/api/v1/settings/*,每次请求都查 DB)按安全客户端 IP
限流(默认 300 rpm/IP);回环/私网/链路本地地址(反代内部转发地址)
一律跳过计数,杜绝把整条反代链路合并进同一个桶造成大面积误拦截。
3. 修复既有隐患:auth 入口限流的 IP 取值从 c.ClientIP() 切换到与审计日志/
会话绑定/API Key ACL 同源的安全客户端 IP 解析(尊重后台「信任反代转发
IP」开关快照)。原实现下默认反代部署(未配置 server.trusted_proxies)
所有用户共享同一个登录限流桶,既会全员误拦也可被单人恶意占满形成登录
DoS;开关关闭时行为与原来完全一致。
工程约束:
- 配置热路径走进程内缓存(atomic.Value + singleflight,60s TTL),
限流中间件零 DB 访问;保存后当前节点立即生效
- 面板限流 Redis 故障 fail-open(auth 入口保持原有 fail-close)
- 429 响应携带 Retry-After;错误码 RATE_LIMITED
- 支付 webhook / 公开支付回调有意不挂限流
- 新增 GET/PUT /api/v1/admin/settings/panel-rate-limit;设置页安全 tab
新增「面板接口限流」卡片(zh/en i18n 全量)
测试:rate_limiter/panel_rate_limit/setting_panel_rate_limit 单测全绿;
routes、handler/admin、-tags unit 契约测试通过;前端 vue-tsc/ESLint/
SettingsView spec(26/26,含新增交互用例)/i18n 守卫全部通过。
137 lines
4.6 KiB
Go
137 lines
4.6 KiB
Go
package routes
|
|
|
|
import (
|
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// RegisterUserRoutes 注册用户相关路由(需要认证)
|
|
func RegisterUserRoutes(
|
|
v1 *gin.RouterGroup,
|
|
h *handler.Handlers,
|
|
jwtAuth middleware.JWTAuthMiddleware,
|
|
auditLog middleware.AuditLogMiddleware,
|
|
settingService *service.SettingService,
|
|
panelRateLimiter *middleware.PanelRateLimiter,
|
|
) {
|
|
authenticated := v1.Group("")
|
|
authenticated.Use(gin.HandlerFunc(jwtAuth))
|
|
authenticated.Use(middleware.BackendModeUserGuard(settingService))
|
|
// 面板全局按用户限流:防止单个账号高频刷接口打爆数据库
|
|
authenticated.Use(panelRateLimiter.Global())
|
|
// 用户管理面变更类操作入审计(含 TOTP 启用/禁用、step-up 验证、密码修改等安全事件)
|
|
authenticated.Use(gin.HandlerFunc(auditLog))
|
|
{
|
|
// 用户接口
|
|
user := authenticated.Group("/user")
|
|
{
|
|
user.GET("/profile", h.User.GetProfile)
|
|
user.PUT("/password", h.User.ChangePassword)
|
|
user.PUT("", h.User.UpdateProfile)
|
|
user.GET("/aff", h.User.GetAffiliate)
|
|
user.POST("/aff/transfer", h.User.TransferAffiliateQuota)
|
|
user.POST("/account-bindings/email/send-code", h.User.SendEmailBindingCode)
|
|
user.POST("/account-bindings/email", h.User.BindEmailIdentity)
|
|
user.DELETE("/account-bindings/:provider", h.User.UnbindIdentity)
|
|
user.POST("/auth-identities/bind/start", h.User.StartIdentityBinding)
|
|
user.GET("/api-keys/:id/usage/daily", panelRateLimiter.Heavy(), h.Usage.GetMyAPIKeyDailyUsage)
|
|
user.GET("/platform-quotas", h.User.GetMyPlatformQuotas)
|
|
|
|
// 通知邮箱管理
|
|
notifyEmail := user.Group("/notify-email")
|
|
{
|
|
notifyEmail.POST("/send-code", h.User.SendNotifyEmailCode)
|
|
notifyEmail.POST("/verify", h.User.VerifyNotifyEmail)
|
|
notifyEmail.PUT("/toggle", h.User.ToggleNotifyEmail)
|
|
notifyEmail.DELETE("", h.User.RemoveNotifyEmail)
|
|
}
|
|
|
|
// TOTP 双因素认证
|
|
totp := user.Group("/totp")
|
|
{
|
|
totp.GET("/status", h.Totp.GetStatus)
|
|
totp.GET("/verification-method", h.Totp.GetVerificationMethod)
|
|
totp.POST("/send-code", h.Totp.SendVerifyCode)
|
|
totp.POST("/setup", h.Totp.InitiateSetup)
|
|
totp.POST("/enable", h.Totp.Enable)
|
|
totp.POST("/disable", h.Totp.Disable)
|
|
// 敏感操作二次验证:授予当前会话一段时间的 step-up 权限
|
|
totp.POST("/step-up", h.Totp.StepUp)
|
|
}
|
|
}
|
|
|
|
// API Key管理
|
|
keys := authenticated.Group("/keys")
|
|
{
|
|
keys.GET("", h.APIKey.List)
|
|
keys.GET("/:id", h.APIKey.GetByID)
|
|
keys.POST("", h.APIKey.Create)
|
|
keys.PUT("/:id", h.APIKey.Update)
|
|
keys.DELETE("/:id", h.APIKey.Delete)
|
|
}
|
|
|
|
// 用户可用分组(非管理员接口)
|
|
groups := authenticated.Group("/groups")
|
|
{
|
|
groups.GET("/available", h.APIKey.GetAvailableGroups)
|
|
groups.GET("/rates", h.APIKey.GetUserGroupRates)
|
|
}
|
|
|
|
// 用户可用渠道(非管理员接口)
|
|
channels := authenticated.Group("/channels")
|
|
{
|
|
channels.GET("/available", h.AvailableChannel.List)
|
|
}
|
|
|
|
// 使用记录(聚合统计属重查询,叠加更严格的按用户限流)
|
|
usage := authenticated.Group("/usage")
|
|
usage.Use(panelRateLimiter.Heavy())
|
|
{
|
|
usage.GET("", h.Usage.List)
|
|
usage.GET("/errors", h.Usage.ListErrors)
|
|
usage.GET("/errors/:id", h.Usage.GetErrorDetail)
|
|
usage.GET("/:id", h.Usage.GetByID)
|
|
usage.GET("/stats", h.Usage.Stats)
|
|
// User dashboard endpoints
|
|
usage.GET("/dashboard/stats", h.Usage.DashboardStats)
|
|
usage.GET("/dashboard/trend", h.Usage.DashboardTrend)
|
|
usage.GET("/dashboard/models", h.Usage.DashboardModels)
|
|
usage.GET("/dashboard/snapshot-v2", h.Usage.DashboardSnapshotV2)
|
|
usage.POST("/dashboard/api-keys-usage", h.Usage.DashboardAPIKeysUsage)
|
|
}
|
|
|
|
// 公告(用户可见)
|
|
announcements := authenticated.Group("/announcements")
|
|
{
|
|
announcements.GET("", h.Announcement.List)
|
|
announcements.POST("/:id/read", h.Announcement.MarkRead)
|
|
}
|
|
|
|
// 卡密兑换
|
|
redeem := authenticated.Group("/redeem")
|
|
{
|
|
redeem.POST("", h.Redeem.Redeem)
|
|
redeem.GET("/history", h.Redeem.GetHistory)
|
|
}
|
|
|
|
// 用户订阅
|
|
subscriptions := authenticated.Group("/subscriptions")
|
|
{
|
|
subscriptions.GET("", h.Subscription.List)
|
|
subscriptions.GET("/active", h.Subscription.GetActive)
|
|
subscriptions.GET("/progress", h.Subscription.GetProgress)
|
|
subscriptions.GET("/summary", h.Subscription.GetSummary)
|
|
}
|
|
|
|
// 渠道监控(用户只读)
|
|
monitors := authenticated.Group("/channel-monitors")
|
|
{
|
|
monitors.GET("", h.ChannelMonitor.List)
|
|
monitors.GET("/:id/status", h.ChannelMonitor.GetStatus)
|
|
}
|
|
}
|
|
}
|