服务端安全加固:多维限流(按 IP/按门店)+ 敏感接口独立速率上限抵御 DDoS/刷接口; 登录暴力破解新增按来源 IP 锁定;反代后正确识别真实客户端 IP; 门店 custom_fields 轻量配置(录入默认值)透传保存。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/wangjia/jiu/backend/config"
|
||||
"github.com/wangjia/jiu/backend/internal/handler"
|
||||
"github.com/wangjia/jiu/backend/internal/middleware"
|
||||
"github.com/wangjia/jiu/backend/internal/service"
|
||||
@@ -46,31 +47,43 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
// 版本信息(无需认证,用于客户端更新检查)
|
||||
r.GET("/version", handler.GetVersion)
|
||||
|
||||
// 限流维度(按真实客户端 IP,依赖 main.go 的 SetTrustedProxies)。
|
||||
// 防爆破/防刷写/防爬:未鉴权接口逐条挂 per-IP 限流,公开商品列表最紧。
|
||||
rl := config.C.RateLimit
|
||||
publicReadIP := middleware.RateLimitByIP(middleware.PerMinute(rl.PublicReadPerMin), rl.PublicReadPerMin)
|
||||
shopListIP := middleware.RateLimitByIP(middleware.PerMinute(rl.ShopListPerMin), rl.ShopListPerMin)
|
||||
loginIP := middleware.RateLimitByIP(middleware.PerMinute(rl.LoginPerMin), rl.LoginPerMin)
|
||||
refreshIP := middleware.RateLimitByIP(middleware.PerMinute(rl.RefreshPerMin), rl.RefreshPerMin)
|
||||
registerIP := middleware.RateLimitByIP(middleware.PerMinute(rl.RegisterPerMin), rl.RegisterPerMin)
|
||||
errorsIP := middleware.RateLimitByIP(middleware.PerMinute(rl.ErrorsPerMin), rl.ErrorsPerMin)
|
||||
|
||||
// 公开商品详情页(注入 OG 标签的 Flutter index.html,供社交分享爬虫读取)
|
||||
r.GET("/product/:public_id", publicH.ProductPage)
|
||||
r.GET("/product/:public_id", publicReadIP, publicH.ProductPage)
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
|
||||
// 公开路由(无需登录)
|
||||
// 公开路由(无需登录):登录/刷新按 IP 限流(与 service 层失败锁定互补)
|
||||
auth := v1.Group("/auth")
|
||||
{
|
||||
auth.POST("/login", authH.Login)
|
||||
auth.POST("/refresh", authH.Refresh)
|
||||
auth.POST("/login", loginIP, authH.Login)
|
||||
auth.POST("/refresh", refreshIP, authH.Refresh)
|
||||
}
|
||||
|
||||
// 公开接口(无需登录)
|
||||
// 公开接口(无需登录):读接口防爬、写接口防刷
|
||||
public := v1.Group("/public")
|
||||
{
|
||||
public.GET("/products/:public_id", publicH.GetProduct)
|
||||
public.GET("/shops/:shop_code/products", publicH.ListShopProducts)
|
||||
public.GET("/release", publicH.GetRelease)
|
||||
public.POST("/errors", errorReportH.Submit)
|
||||
public.POST("/register", authH.Register)
|
||||
public.GET("/products/:public_id", publicReadIP, publicH.GetProduct)
|
||||
public.GET("/shops/:shop_code/products", shopListIP, publicH.ListShopProducts)
|
||||
public.GET("/release", publicReadIP, publicH.GetRelease)
|
||||
public.POST("/errors", errorsIP, errorReportH.Submit)
|
||||
public.POST("/register", registerIP, authH.Register)
|
||||
}
|
||||
|
||||
// 需要 JWT 的基础路由组
|
||||
api := v1.Group("")
|
||||
api.Use(middleware.JWT(db))
|
||||
// 每店限流(按 shop_id,挂在 JWT 之后):防单店打爆共享后端,保障多租户公平
|
||||
api.Use(middleware.RateLimitByShop(middleware.PerSecond(rl.ShopRPS), rl.ShopBurst))
|
||||
|
||||
// 会话/在线状态(豁免 LicenseGuard:登出/心跳/在线列表在锁定期也要可用)
|
||||
{
|
||||
@@ -179,11 +192,11 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
// 财务
|
||||
finance := api.Group("/finance")
|
||||
{
|
||||
finance.GET("/records", financeH.ListRecords)
|
||||
finance.POST("/records", financeH.Create)
|
||||
finance.PUT("/records/:id/close", financeH.Close)
|
||||
finance.PUT("/records/close-by-ref", financeH.CloseByRef)
|
||||
finance.GET("/summary", financeH.Summary)
|
||||
finance.GET("/records", financeH.ListRecords)
|
||||
finance.POST("/records", financeH.Create)
|
||||
finance.PUT("/records/:id/close", financeH.Close)
|
||||
finance.PUT("/records/close-by-ref", financeH.CloseByRef)
|
||||
finance.GET("/summary", financeH.Summary)
|
||||
}
|
||||
|
||||
// 酒行信息
|
||||
@@ -211,15 +224,15 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
// 导入
|
||||
imp := api.Group("/import")
|
||||
{
|
||||
imp.POST("/products", importH.ImportProducts)
|
||||
imp.POST("/partners", importH.ImportPartners)
|
||||
imp.POST("/product-names", importH.ImportProductNames)
|
||||
imp.POST("/products", importH.ImportProducts)
|
||||
imp.POST("/partners", importH.ImportPartners)
|
||||
imp.POST("/product-names", importH.ImportProductNames)
|
||||
imp.POST("/product-series", importH.ImportProductSeries)
|
||||
imp.POST("/product-specs", importH.ImportProductSpecs)
|
||||
imp.POST("/product-codes", importH.ImportProductCodes)
|
||||
imp.POST("/stock-in", importH.ImportStockIn)
|
||||
imp.POST("/stock-out", importH.ImportStockOut)
|
||||
imp.POST("/inventory", importH.ImportInventory)
|
||||
imp.POST("/product-specs", importH.ImportProductSpecs)
|
||||
imp.POST("/product-codes", importH.ImportProductCodes)
|
||||
imp.POST("/stock-in", importH.ImportStockIn)
|
||||
imp.POST("/stock-out", importH.ImportStockOut)
|
||||
imp.POST("/inventory", importH.ImportInventory)
|
||||
}
|
||||
|
||||
// 基础数据选项(名称/系列/规格)
|
||||
|
||||
Reference in New Issue
Block a user