mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 07:33:06 +08: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 守卫全部通过。
114 lines
4.2 KiB
Go
114 lines
4.2 KiB
Go
package routes
|
|
|
|
import (
|
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
|
"github.com/Wei-Shaw/sub2api/internal/handler/admin"
|
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// RegisterPaymentRoutes registers all payment-related routes:
|
|
// user-facing endpoints, webhook endpoints, and admin endpoints.
|
|
func RegisterPaymentRoutes(
|
|
v1 *gin.RouterGroup,
|
|
paymentHandler *handler.PaymentHandler,
|
|
webhookHandler *handler.PaymentWebhookHandler,
|
|
adminPaymentHandler *admin.PaymentHandler,
|
|
jwtAuth middleware.JWTAuthMiddleware,
|
|
adminAuth middleware.AdminAuthMiddleware,
|
|
auditLog middleware.AuditLogMiddleware,
|
|
settingService *service.SettingService,
|
|
panelRateLimiter *middleware.PanelRateLimiter,
|
|
) {
|
|
// --- User-facing payment endpoints (authenticated) ---
|
|
authenticated := v1.Group("/payment")
|
|
authenticated.Use(gin.HandlerFunc(jwtAuth))
|
|
authenticated.Use(middleware.BackendModeUserGuard(settingService))
|
|
// 面板全局按用户限流
|
|
authenticated.Use(panelRateLimiter.Global())
|
|
{
|
|
authenticated.GET("/config", paymentHandler.GetPaymentConfig)
|
|
authenticated.GET("/checkout-info", paymentHandler.GetCheckoutInfo)
|
|
authenticated.GET("/plans", paymentHandler.GetPlans)
|
|
authenticated.GET("/limits", paymentHandler.GetLimits)
|
|
|
|
orders := authenticated.Group("/orders")
|
|
{
|
|
orders.POST("", paymentHandler.CreateOrder)
|
|
orders.POST("/verify", paymentHandler.VerifyOrder)
|
|
orders.GET("/my", paymentHandler.GetMyOrders)
|
|
orders.GET("/:id", paymentHandler.GetOrder)
|
|
orders.POST("/:id/cancel", paymentHandler.CancelOrder)
|
|
orders.POST("/:id/refund-request", paymentHandler.RequestRefund)
|
|
orders.GET("/refund-eligible-providers", paymentHandler.GetRefundEligibleProviders)
|
|
}
|
|
}
|
|
|
|
// --- Public payment endpoints (no auth) ---
|
|
// Signed resume-token recovery is the preferred public lookup path.
|
|
// The legacy anonymous out_trade_no verify endpoint remains available as a
|
|
// persisted-state compatibility path for staggered upgrades.
|
|
public := v1.Group("/payment/public")
|
|
{
|
|
public.POST("/orders/verify", paymentHandler.VerifyOrderPublic)
|
|
public.POST("/orders/resolve", paymentHandler.ResolveOrderPublicByResumeToken)
|
|
}
|
|
|
|
// --- Webhook endpoints (no auth) ---
|
|
webhook := v1.Group("/payment/webhook")
|
|
{
|
|
// EasyPay sends GET callbacks with query params
|
|
webhook.GET("/easypay", webhookHandler.EasyPayNotify)
|
|
webhook.POST("/easypay", webhookHandler.EasyPayNotify)
|
|
webhook.POST("/alipay", webhookHandler.AlipayNotify)
|
|
webhook.POST("/wxpay", webhookHandler.WxpayNotify)
|
|
webhook.POST("/stripe", webhookHandler.StripeWebhook)
|
|
webhook.POST("/airwallex", webhookHandler.AirwallexWebhook)
|
|
}
|
|
|
|
// --- Admin payment endpoints (admin auth) ---
|
|
adminGroup := v1.Group("/admin/payment")
|
|
adminGroup.Use(gin.HandlerFunc(adminAuth))
|
|
adminGroup.Use(gin.HandlerFunc(auditLog))
|
|
adminGroup.Use(middleware.AdminComplianceGuard(settingService))
|
|
{
|
|
// Dashboard
|
|
adminGroup.GET("/dashboard", adminPaymentHandler.GetDashboard)
|
|
|
|
// Config
|
|
adminGroup.GET("/config", adminPaymentHandler.GetConfig)
|
|
adminGroup.PUT("/config", adminPaymentHandler.UpdateConfig)
|
|
|
|
// Orders
|
|
adminOrders := adminGroup.Group("/orders")
|
|
{
|
|
adminOrders.GET("", adminPaymentHandler.ListOrders)
|
|
adminOrders.GET("/:id", adminPaymentHandler.GetOrderDetail)
|
|
adminOrders.POST("/:id/cancel", adminPaymentHandler.CancelOrder)
|
|
adminOrders.POST("/:id/retry", adminPaymentHandler.RetryFulfillment)
|
|
adminOrders.POST("/:id/refund", adminPaymentHandler.ProcessRefund)
|
|
adminOrders.POST("/:id/refund/query", adminPaymentHandler.QueryAndFinalizeRefund)
|
|
}
|
|
|
|
// Subscription Plans
|
|
plans := adminGroup.Group("/plans")
|
|
{
|
|
plans.GET("", adminPaymentHandler.ListPlans)
|
|
plans.POST("", adminPaymentHandler.CreatePlan)
|
|
plans.PUT("/:id", adminPaymentHandler.UpdatePlan)
|
|
plans.DELETE("/:id", adminPaymentHandler.DeletePlan)
|
|
}
|
|
|
|
// Provider Instances
|
|
providers := adminGroup.Group("/providers")
|
|
{
|
|
providers.GET("", adminPaymentHandler.ListProviders)
|
|
providers.POST("", adminPaymentHandler.CreateProvider)
|
|
providers.PUT("/:id", adminPaymentHandler.UpdateProvider)
|
|
providers.DELETE("/:id", adminPaymentHandler.DeleteProvider)
|
|
}
|
|
}
|
|
}
|