From b77d64bc9f9cdd3cc0057598db474a87e44ceff2 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Sun, 10 Aug 2025 19:15:26 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E6=B3=A8=E5=86=8C=E6=97=B6=E5=8F=91?= =?UTF-8?q?=E9=80=81=E9=82=AE=E4=BB=B6=E9=AA=8C=E8=AF=81=E7=A0=81=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E7=AD=89=E5=BE=85=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/auth/RegisterForm.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/web/src/components/auth/RegisterForm.js b/web/src/components/auth/RegisterForm.js index 897881ad..071631c6 100644 --- a/web/src/components/auth/RegisterForm.js +++ b/web/src/components/auth/RegisterForm.js @@ -80,6 +80,8 @@ const RegisterForm = () => { const [verificationCodeLoading, setVerificationCodeLoading] = useState(false); const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false); const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); + const [disableButton, setDisableButton] = useState(false); + const [countdown, setCountdown] = useState(30); const logo = getLogo(); const systemName = getSystemName(); @@ -106,6 +108,19 @@ const RegisterForm = () => { } }, [status]); + useEffect(() => { + let countdownInterval = null; + if (disableButton && countdown > 0) { + countdownInterval = setInterval(() => { + setCountdown(countdown - 1); + }, 1000); + } else if (countdown === 0) { + setDisableButton(false); + setCountdown(30); + } + return () => clearInterval(countdownInterval); // Clean up on unmount + }, [disableButton, countdown]); + const onWeChatLoginClicked = () => { setWechatLoading(true); setShowWeChatLoginModal(true); @@ -198,6 +213,7 @@ const RegisterForm = () => { const { success, message } = res.data; if (success) { showSuccess('验证码发送成功,请检查你的邮箱!'); + setDisableButton(true); // 发送成功后禁用按钮,开始倒计时 } else { showError(message); } @@ -454,9 +470,10 @@ const RegisterForm = () => { } /> From 543e7b0b6b6f634623924ad0c77fd34b0a20bf76 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Sun, 10 Aug 2025 21:22:53 +0800 Subject: [PATCH 2/3] 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) From 6ea19b0ae20afe36bd1052d9cce233cd2a49edec Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Sun, 10 Aug 2025 23:18:09 +0800 Subject: [PATCH 3/3] feat(middleware): redis atomic incr, show waiting time --- middleware/email-verification-rate-limit.go | 26 ++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/middleware/email-verification-rate-limit.go b/middleware/email-verification-rate-limit.go index 5885d5a5..a7d828d9 100644 --- a/middleware/email-verification-rate-limit.go +++ b/middleware/email-verification-rate-limit.go @@ -21,24 +21,34 @@ func redisEmailVerificationRateLimiter(c *gin.Context) { rdb := common.RDB key := "emailVerification:" + EmailVerificationRateLimitMark + ":" + c.ClientIP() - listLength, err := rdb.LLen(ctx, key).Result() + count, err := rdb.Incr(ctx, key).Result() if err != nil { - fmt.Println("Redis限流检查失败:", err.Error()) - c.Status(http.StatusInternalServerError) - c.Abort() + // fallback + memoryEmailVerificationRateLimiter(c) return } - if listLength < EmailVerificationMaxRequests { - rdb.LPush(ctx, key, time.Now().Format(timeFormat)) - rdb.Expire(ctx, key, time.Duration(EmailVerificationDuration)*time.Second) + // 第一次设置键时设置过期时间 + if count == 1 { + _ = rdb.Expire(ctx, key, time.Duration(EmailVerificationDuration)*time.Second).Err() + } + + // 检查是否超出限制 + if count <= int64(EmailVerificationMaxRequests) { c.Next() return } + // 获取剩余等待时间 + ttl, err := rdb.TTL(ctx, key).Result() + waitSeconds := int64(EmailVerificationDuration) + if err == nil && ttl > 0 { + waitSeconds = int64(ttl.Seconds()) + } + c.JSON(http.StatusTooManyRequests, gin.H{ "success": false, - "message": fmt.Sprintf("发送过于频繁,请等待 %d 秒后再试", EmailVerificationDuration), + "message": fmt.Sprintf("发送过于频繁,请等待 %d 秒后再试", waitSeconds), }) c.Abort() }