From 543e7b0b6b6f634623924ad0c77fd34b0a20bf76 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Sun, 10 Aug 2025 21:22:53 +0800 Subject: [PATCH] feat(middleware): add email verification rate limit --- middleware/email-verification-rate-limit.go | 70 +++++++++++++++++++++ router/api-router.go | 30 ++++----- 2 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 middleware/email-verification-rate-limit.go diff --git a/middleware/email-verification-rate-limit.go b/middleware/email-verification-rate-limit.go new file mode 100644 index 00000000..5885d5a5 --- /dev/null +++ b/middleware/email-verification-rate-limit.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "context" + "fmt" + "net/http" + "one-api/common" + "time" + + "github.com/gin-gonic/gin" +) + +const ( + EmailVerificationRateLimitMark = "EV" + EmailVerificationMaxRequests = 2 // 30秒内最多2次 + EmailVerificationDuration = 30 // 30秒时间窗口 +) + +func redisEmailVerificationRateLimiter(c *gin.Context) { + ctx := context.Background() + rdb := common.RDB + key := "emailVerification:" + EmailVerificationRateLimitMark + ":" + c.ClientIP() + + listLength, err := rdb.LLen(ctx, key).Result() + if err != nil { + fmt.Println("Redis限流检查失败:", err.Error()) + c.Status(http.StatusInternalServerError) + c.Abort() + return + } + + if listLength < EmailVerificationMaxRequests { + rdb.LPush(ctx, key, time.Now().Format(timeFormat)) + rdb.Expire(ctx, key, time.Duration(EmailVerificationDuration)*time.Second) + c.Next() + return + } + + c.JSON(http.StatusTooManyRequests, gin.H{ + "success": false, + "message": fmt.Sprintf("发送过于频繁,请等待 %d 秒后再试", EmailVerificationDuration), + }) + c.Abort() +} + +func memoryEmailVerificationRateLimiter(c *gin.Context) { + key := EmailVerificationRateLimitMark + ":" + c.ClientIP() + + if !inMemoryRateLimiter.Request(key, EmailVerificationMaxRequests, EmailVerificationDuration) { + c.JSON(http.StatusTooManyRequests, gin.H{ + "success": false, + "message": "发送过于频繁,请稍后再试", + }) + c.Abort() + return + } + + c.Next() +} + +func EmailVerificationRateLimit() gin.HandlerFunc { + return func(c *gin.Context) { + if common.RedisEnabled { + redisEmailVerificationRateLimiter(c) + } else { + inMemoryRateLimiter.Init(common.RateLimitKeyExpirationDuration) + memoryEmailVerificationRateLimiter(c) + } + } +} diff --git a/router/api-router.go b/router/api-router.go index e8519e23..aa3cba6d 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -24,7 +24,7 @@ func SetApiRouter(router *gin.Engine) { //apiRouter.GET("/midjourney", controller.GetMidjourney) apiRouter.GET("/home_page_content", controller.GetHomePageContent) apiRouter.GET("/pricing", middleware.TryUserAuth(), controller.GetPricing) - apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) + apiRouter.GET("/verification", middleware.EmailVerificationRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth) @@ -67,7 +67,7 @@ func SetApiRouter(router *gin.Engine) { selfRoute.POST("/stripe/amount", controller.RequestStripeAmount) selfRoute.POST("/aff_transfer", controller.TransferAffQuota) selfRoute.PUT("/setting", controller.UpdateUserSetting) - + // 2FA routes selfRoute.GET("/2fa/status", controller.Get2FAStatus) selfRoute.POST("/2fa/setup", controller.Setup2FA) @@ -86,7 +86,7 @@ func SetApiRouter(router *gin.Engine) { adminRoute.POST("/manage", controller.ManageUser) adminRoute.PUT("/", controller.UpdateUser) adminRoute.DELETE("/:id", controller.DeleteUser) - + // Admin 2FA routes adminRoute.GET("/2fa/stats", controller.Admin2FAStats) adminRoute.DELETE("/:id/2fa", controller.AdminDisable2FA) @@ -200,22 +200,22 @@ func SetApiRouter(router *gin.Engine) { } vendorRoute := apiRouter.Group("/vendors") - vendorRoute.Use(middleware.AdminAuth()) - { - vendorRoute.GET("/", controller.GetAllVendors) - vendorRoute.GET("/search", controller.SearchVendors) - vendorRoute.GET("/:id", controller.GetVendorMeta) - vendorRoute.POST("/", controller.CreateVendorMeta) - vendorRoute.PUT("/", controller.UpdateVendorMeta) - vendorRoute.DELETE("/:id", controller.DeleteVendorMeta) - } + vendorRoute.Use(middleware.AdminAuth()) + { + vendorRoute.GET("/", controller.GetAllVendors) + vendorRoute.GET("/search", controller.SearchVendors) + vendorRoute.GET("/:id", controller.GetVendorMeta) + vendorRoute.POST("/", controller.CreateVendorMeta) + vendorRoute.PUT("/", controller.UpdateVendorMeta) + vendorRoute.DELETE("/:id", controller.DeleteVendorMeta) + } - modelsRoute := apiRouter.Group("/models") + modelsRoute := apiRouter.Group("/models") modelsRoute.Use(middleware.AdminAuth()) { modelsRoute.GET("/missing", controller.GetMissingModels) - modelsRoute.GET("/", controller.GetAllModelsMeta) - modelsRoute.GET("/search", controller.SearchModelsMeta) + modelsRoute.GET("/", controller.GetAllModelsMeta) + modelsRoute.GET("/search", controller.SearchModelsMeta) modelsRoute.GET("/:id", controller.GetModelMeta) modelsRoute.POST("/", controller.CreateModelMeta) modelsRoute.PUT("/", controller.UpdateModelMeta)