Files
opsx-licence/backend/router/router.go
T
Logan.Li 1c50d565eb
CI / backend (push) Canceled after 0s
fix(security): webhook fail-closed、机密字段信封加密、限流信任代理修正
三项 P0 安全修复:

1. Webhook 验签 fail-closed
   - WaffoWebhook 未配置验签材料时从「告警放行」改为 503 拒绝,
     封死伪造 order.completed 结算订单的路径
   - 通用 PaymentWebhook 从「仅 release 拒绝」收紧为默认拒绝,
     仅显式 ALLOW_UNVERIFIED_WEBHOOK=1(本地调试)放行

2. 机密字段信封加密(新增 internal/secretbox)
   - AES-256-GCM,主密钥 OPSLIC_MASTER_KEY(64 位 hex),密文带
     enc:v1: 前缀;Encrypt 幂等、历史明文原样放行
   - PaymentGatewayConfig.SecretKey/WebhookSecret 与 PeerNode.
     SharedKey 挂 GORM 钩子透明加解密,业务代码零改动;SharedKey
     列宽 128→512(密文更长)
   - 启动 fail-fast 校验主密钥;MigratePlaintext 幂等迁移存量明文;
     节点同步只含业务表,机密表为节点本地,互不影响
   - docker-compose/.env.example 补 OPSLIC_MASTER_KEY 与说明

3. 限流信任代理修正
   - 默认 SetTrustedProxies(nil) 不信任任何 X-Forwarded-For,
     封死伪造 XFF 轮换 IP 绕过按 IP 限流;反代节点用
     TRUSTED_PROXIES 指定内网 CIDR,非法配置拒绝启动

验证:go build/vet/test 全绿(含 secretbox 6 条单测);
读取路径(Waffo 私钥/webhook 密钥/互联密钥)确认全走 GORM 钩子。

部署注意:上线须配置 OPSLIC_MASTER_KEY(双节点一致)与
WAFFO_WEBHOOK_PUBLIC_KEY(未配则回调 503,属预期);香港反代
节点配 TRUSTED_PROXIES。
2026-11-01 02:03:26 +08:00

296 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package router
import (
"backend/config"
"backend/internal/controller/analytics"
"backend/internal/controller/auth"
"backend/internal/controller/customer"
"backend/internal/controller/entitlement"
"backend/internal/controller/license"
"backend/internal/controller/order"
"backend/internal/controller/plan"
"backend/internal/controller/product"
"backend/internal/controller/setting"
synccontroller "backend/internal/controller/sync"
"backend/middleware"
"os"
"strings"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// register 同时注册带和不带 trailing slash 的路由 // Logan.Li
func register(r gin.IRoutes, method, relativePath string, handlers ...gin.HandlerFunc) {
isParamRoute := strings.Contains(relativePath, ":")
switch method {
case "GET":
r.GET(relativePath, handlers...)
if !isParamRoute {
r.GET(relativePath+"/", handlers...)
}
case "POST":
r.POST(relativePath, handlers...)
if !isParamRoute {
r.POST(relativePath+"/", handlers...)
}
case "PUT":
r.PUT(relativePath, handlers...)
if !isParamRoute {
r.PUT(relativePath+"/", handlers...)
}
case "DELETE":
r.DELETE(relativePath, handlers...)
if !isParamRoute {
r.DELETE(relativePath+"/", handlers...)
}
case "PATCH":
r.PATCH(relativePath, handlers...)
if !isParamRoute {
r.PATCH(relativePath+"/", handlers...)
}
}
}
// SetupRouter 初始化路由 // Logan.Li
func SetupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
gin.SetMode(cfg.Mode)
r := gin.Default()
r.RedirectTrailingSlash = false
r.RedirectFixedPath = false
// 信任代理:默认不信任任何代理头(X-Forwarded-For 可伪造,会绕过按 IP 限流);
// 经 nginx 反代的节点用 TRUSTED_PROXIES 指定其内网 CIDR,多个用逗号分隔 // Logan.Li
if proxies := strings.TrimSpace(os.Getenv("TRUSTED_PROXIES")); proxies != "" {
if err := r.SetTrustedProxies(strings.Split(proxies, ",")); err != nil {
panic("TRUSTED_PROXIES 配置非法: " + err.Error())
}
} else if err := r.SetTrustedProxies(nil); err != nil {
panic("SetTrustedProxies 失败: " + err.Error())
}
// Gzip 压缩中间件 // Logan.Li
r.Use(gzip.Gzip(gzip.DefaultCompression))
// CORS 中间件 // Logan.Li
r.Use(middleware.CORSMiddleware(cfg))
// 存活探针(不查库) // Logan.Li
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"code": 0, "message": "ok", "data": nil})
})
r.GET("/healthz", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "alive"})
})
// 就绪探针:检查数据库可达 // Logan.Li
r.GET("/readyz", func(c *gin.Context) {
sqlDB, err := db.DB()
if err != nil || sqlDB.Ping() != nil {
c.JSON(503, gin.H{"status": "db unreachable"})
return
}
c.JSON(200, gin.H{"status": "ready"})
})
// 初始化控制器 // Logan.Li
authController := auth.NewAuthController(db, cfg)
productController := product.NewProductController(db)
customerController := customer.NewCustomerController(db)
licenseController := license.NewLicenseController(db, cfg)
planController := plan.NewPlanController(db)
orderController := order.NewOrderController(db)
entitlementController := entitlement.NewEntitlementController(db)
analyticsController := analytics.NewAnalyticsController(db)
settingController := setting.NewSettingController(db)
syncController := synccontroller.NewController(db, cfg)
api := r.Group("/api")
{
// ============ 节点互联(对端调用,HMAC 签名认证,非 JWT) ============
syncGroup := api.Group("/v1/sync")
{
syncGroup.POST("/handshake", middleware.RateLimit(1, 30), syncController.HandshakeIn)
syncGroup.POST("/exchange", middleware.RateLimit(2, 60), syncController.ExchangeIn)
}
// ============ 认证路由(无需 JWT) ============
authGroup := api.Group("/auth")
{
register(authGroup, "POST", "/login", middleware.RateLimit(0.2, 5), authController.Login)
}
// ============ 客户端授权契约(无需认证,供外部工具调用) ============
clientAPI := api.Group("/v1")
{
licLimit := middleware.RateLimit(2, 30)
checkoutLimit := middleware.RateLimit(0.5, 10)
webhookLimit := middleware.RateLimit(1, 20)
// License 验证 / 激活 / 停用 / 心跳
register(clientAPI, "POST", "/license/verify", licLimit, licenseController.Verify)
register(clientAPI, "POST", "/license/activate", licLimit, licenseController.ActivateDevice)
register(clientAPI, "POST", "/license/deactivate", licLimit, licenseController.Deactivate)
register(clientAPI, "POST", "/license/heartbeat", licLimit, licenseController.Heartbeat)
register(clientAPI, "POST", "/license/devices", licLimit, licenseController.ListDevices)
// 产品定价查询(展示用)
register(clientAPI, "GET", "/products/code/:code", productController.GetByCode)
clientAPI.GET("/products/:product_id/plans", planController.GetByProduct)
// 订单与支付(客户端购买入口 + 支付回调)
register(clientAPI, "POST", "/orders", checkoutLimit, orderController.CreateOrder)
register(clientAPI, "POST", "/checkout", checkoutLimit, orderController.Checkout)
register(clientAPI, "POST", "/purchase/claim", checkoutLimit, orderController.ClaimPurchase)
register(clientAPI, "POST", "/webhooks/payment", webhookLimit, orderController.PaymentWebhook)
register(clientAPI, "GET", "/webhooks/waffo", orderController.WaffoWebhookReady)
register(clientAPI, "POST", "/webhooks/waffo", webhookLimit, orderController.WaffoWebhook)
register(clientAPI, "GET", "/purchase/success", orderController.PurchaseSuccess)
register(clientAPI, "POST", "/purchase/check", checkoutLimit, orderController.CheckDuplicatePurchase)
register(clientAPI, "POST", "/purchase/status", checkoutLimit, orderController.CheckPurchaseStatus)
// 客户权益查询(通过邮箱)
register(clientAPI, "GET", "/entitlements/my", checkoutLimit, entitlementController.GetByEmail)
}
// ============ 需要认证的管理路由 ============
protected := api.Group("", middleware.AuthMiddleware(cfg, db))
{
// 认证相关
register(protected, "GET", "/auth/me", authController.Me)
register(protected, "PUT", "/auth/password", authController.ChangePassword)
// 产品管理
products := protected.Group("/products")
{
register(products, "GET", "", productController.List)
register(products, "GET", "/all", productController.GetAll)
register(products, "POST", "", productController.Create)
register(products, "GET", "/:id", productController.Get)
register(products, "PUT", "/:id", productController.Update)
register(products, "DELETE", "/:id", productController.Delete)
register(products, "POST", "/batch-delete", productController.BatchDelete)
register(products, "PATCH", "/:id/status", productController.UpdateStatus)
}
// 客户管理
customers := protected.Group("/customers")
{
register(customers, "GET", "", customerController.List)
register(customers, "GET", "/all", customerController.GetAll)
register(customers, "POST", "", customerController.Create)
register(customers, "GET", "/:id", customerController.Get)
register(customers, "PUT", "/:id", customerController.Update)
register(customers, "DELETE", "/:id", customerController.Delete)
register(customers, "POST", "/batch-delete", customerController.BatchDelete)
register(customers, "PATCH", "/:id/status", customerController.UpdateStatus)
}
// License 管理
licenses := protected.Group("/licenses")
{
register(licenses, "GET", "", licenseController.List)
register(licenses, "POST", "", licenseController.Create)
register(licenses, "POST", "/offline", licenseController.IssueOffline)
register(licenses, "GET", "/offline/public-key", licenseController.OfflinePublicKey)
register(licenses, "GET", "/export", licenseController.Export)
register(licenses, "POST", "/batch-delete", licenseController.BatchDelete)
register(licenses, "GET", "/:id", licenseController.Get)
register(licenses, "PUT", "/:id", licenseController.Update)
register(licenses, "DELETE", "/:id", licenseController.Delete)
register(licenses, "POST", "/:id/renew", licenseController.Renew)
register(licenses, "POST", "/:id/revoke", licenseController.Revoke)
register(licenses, "POST", "/:id/suspend", licenseController.Suspend)
register(licenses, "POST", "/:id/activate", licenseController.Activate)
register(licenses, "GET", "/:id/activations", licenseController.GetActivations)
register(licenses, "POST", "/:id/activations/:activationId/offline", licenseController.ForceOffline)
}
// 定价计划管理
plans := protected.Group("/plans")
{
register(plans, "GET", "", planController.List)
register(plans, "POST", "", planController.Create)
register(plans, "PUT", "/:id", planController.Update)
register(plans, "DELETE", "/:id", planController.Delete)
register(plans, "PATCH", "/:id/status", planController.ToggleStatus)
}
// 订单管理
orders := protected.Group("/orders")
{
register(orders, "GET", "", orderController.List)
register(orders, "GET", "/:id", orderController.GetDetail)
register(orders, "POST", "/manual-grant", orderController.ManualGrant)
register(orders, "POST", "/batch-delete", orderController.BatchDelete)
}
// 客户权益管理
entitlements := protected.Group("/entitlements")
{
register(entitlements, "GET", "", entitlementController.List)
register(entitlements, "GET", "/:id", entitlementController.Get)
register(entitlements, "POST", "/:id/revoke", entitlementController.Revoke)
register(entitlements, "POST", "/:id/renew", entitlementController.Renew)
}
// 仪表板与数据分析
dashboard := protected.Group("/dashboard")
{
register(dashboard, "GET", "/stats", analyticsController.GetDashboardStats)
register(dashboard, "GET", "/expiring", analyticsController.GetExpiringLicenses)
register(dashboard, "GET", "/activities", analyticsController.GetRecentActivities)
register(dashboard, "GET", "/online-stats", analyticsController.GetOnlineStats)
register(dashboard, "GET", "/revenue", analyticsController.GetRevenueStats)
}
analyticsGroup := protected.Group("/analytics")
{
register(analyticsGroup, "GET", "/license-growth", analyticsController.GetLicenseGrowth)
register(analyticsGroup, "GET", "/license-growth-detail", analyticsController.GetLicenseGrowthDetail)
register(analyticsGroup, "GET", "/product-distribution", analyticsController.GetProductDistribution)
register(analyticsGroup, "GET", "/revenue", analyticsController.GetRevenueStats)
register(analyticsGroup, "GET", "/geo-distribution", analyticsController.GetGeoDistribution)
register(analyticsGroup, "GET", "/platform-distribution", analyticsController.GetPlatformDistribution)
}
// 日志审计与系统设置
logs := protected.Group("/logs")
{
register(logs, "GET", "", settingController.ListAuditLogs)
register(logs, "DELETE", "/clear", settingController.ClearAuditLogs)
}
system := protected.Group("/settings")
{
register(system, "GET", "", settingController.Get)
register(system, "PUT", "", settingController.Update)
register(system, "GET", "/waffo", settingController.GetWaffo)
register(system, "PUT", "/waffo", settingController.UpdateWaffo)
register(system, "POST", "/waffo/test", settingController.DebugWaffo)
}
// 节点互联管理(双向授权同步) // Logan.Li
syncAdmin := protected.Group("/sync")
{
register(syncAdmin, "GET", "/peers", syncController.ListPeers)
register(syncAdmin, "POST", "/peers", syncController.SavePeer)
register(syncAdmin, "DELETE", "/peers/:id", syncController.DeletePeer)
register(syncAdmin, "POST", "/peers/:id/handshake", syncController.TestHandshake)
register(syncAdmin, "POST", "/peers/:id/sync", syncController.SyncNow)
register(syncAdmin, "GET", "/generate-key", syncController.GenerateKey)
}
// 支付调试(与 settings/waffo/test 同一 handler) // Logan.Li
paymentGroup := protected.Group("/payment")
{
register(paymentGroup, "POST", "/waffo/debug", settingController.DebugWaffo)
}
}
}
return r
}