Просмотр исходного кода

Merge pull request #1556 from QuantumNous/fix-register-mail-verifycode-waiting-time

fix: 注册时发送邮件验证码没有等待时间
Calcium-Ion 6 месяцев назад
Родитель
Сommit
7997a04a68
3 измененных файлов с 114 добавлено и 17 удалено
  1. 80 0
      middleware/email-verification-rate-limit.go
  2. 16 16
      router/api-router.go
  3. 18 1
      web/src/components/auth/RegisterForm.js

+ 80 - 0
middleware/email-verification-rate-limit.go

@@ -0,0 +1,80 @@
+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()
+
+	count, err := rdb.Incr(ctx, key).Result()
+	if err != nil {
+		// fallback
+		memoryEmailVerificationRateLimiter(c)
+		return
+	}
+
+	// 第一次设置键时设置过期时间
+	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 秒后再试", waitSeconds),
+	})
+	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)
+		}
+	}
+}

+ 16 - 16
router/api-router.go

@@ -24,7 +24,7 @@ func SetApiRouter(router *gin.Engine) {
 		//apiRouter.GET("/midjourney", controller.GetMidjourney)
 		//apiRouter.GET("/midjourney", controller.GetMidjourney)
 		apiRouter.GET("/home_page_content", controller.GetHomePageContent)
 		apiRouter.GET("/home_page_content", controller.GetHomePageContent)
 		apiRouter.GET("/pricing", middleware.TryUserAuth(), controller.GetPricing)
 		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.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
 		apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
 		apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
 		apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
 		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("/stripe/amount", controller.RequestStripeAmount)
 				selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
 				selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
 				selfRoute.PUT("/setting", controller.UpdateUserSetting)
 				selfRoute.PUT("/setting", controller.UpdateUserSetting)
-				
+
 				// 2FA routes
 				// 2FA routes
 				selfRoute.GET("/2fa/status", controller.Get2FAStatus)
 				selfRoute.GET("/2fa/status", controller.Get2FAStatus)
 				selfRoute.POST("/2fa/setup", controller.Setup2FA)
 				selfRoute.POST("/2fa/setup", controller.Setup2FA)
@@ -86,7 +86,7 @@ func SetApiRouter(router *gin.Engine) {
 				adminRoute.POST("/manage", controller.ManageUser)
 				adminRoute.POST("/manage", controller.ManageUser)
 				adminRoute.PUT("/", controller.UpdateUser)
 				adminRoute.PUT("/", controller.UpdateUser)
 				adminRoute.DELETE("/:id", controller.DeleteUser)
 				adminRoute.DELETE("/:id", controller.DeleteUser)
-				
+
 				// Admin 2FA routes
 				// Admin 2FA routes
 				adminRoute.GET("/2fa/stats", controller.Admin2FAStats)
 				adminRoute.GET("/2fa/stats", controller.Admin2FAStats)
 				adminRoute.DELETE("/:id/2fa", controller.AdminDisable2FA)
 				adminRoute.DELETE("/:id/2fa", controller.AdminDisable2FA)
@@ -200,22 +200,22 @@ func SetApiRouter(router *gin.Engine) {
 		}
 		}
 
 
 		vendorRoute := apiRouter.Group("/vendors")
 		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)
-        }
-
-        modelsRoute := apiRouter.Group("/models")
+		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.Use(middleware.AdminAuth())
 		modelsRoute.Use(middleware.AdminAuth())
 		{
 		{
 			modelsRoute.GET("/missing", controller.GetMissingModels)
 			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.GET("/:id", controller.GetModelMeta)
 			modelsRoute.POST("/", controller.CreateModelMeta)
 			modelsRoute.POST("/", controller.CreateModelMeta)
 			modelsRoute.PUT("/", controller.UpdateModelMeta)
 			modelsRoute.PUT("/", controller.UpdateModelMeta)

+ 18 - 1
web/src/components/auth/RegisterForm.js

@@ -80,6 +80,8 @@ const RegisterForm = () => {
   const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
   const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
   const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false);
   const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false);
   const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
   const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
+  const [disableButton, setDisableButton] = useState(false);
+  const [countdown, setCountdown] = useState(30);
 
 
   const logo = getLogo();
   const logo = getLogo();
   const systemName = getSystemName();
   const systemName = getSystemName();
@@ -106,6 +108,19 @@ const RegisterForm = () => {
     }
     }
   }, [status]);
   }, [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 = () => {
   const onWeChatLoginClicked = () => {
     setWechatLoading(true);
     setWechatLoading(true);
     setShowWeChatLoginModal(true);
     setShowWeChatLoginModal(true);
@@ -198,6 +213,7 @@ const RegisterForm = () => {
       const { success, message } = res.data;
       const { success, message } = res.data;
       if (success) {
       if (success) {
         showSuccess('验证码发送成功,请检查你的邮箱!');
         showSuccess('验证码发送成功,请检查你的邮箱!');
+        setDisableButton(true); // 发送成功后禁用按钮,开始倒计时
       } else {
       } else {
         showError(message);
         showError(message);
       }
       }
@@ -454,9 +470,10 @@ const RegisterForm = () => {
                         <Button
                         <Button
                           onClick={sendVerificationCode}
                           onClick={sendVerificationCode}
                           loading={verificationCodeLoading}
                           loading={verificationCodeLoading}
+                          disabled={disableButton || verificationCodeLoading}
                           size="small"
                           size="small"
                         >
                         >
-                          {t('获取验证码')}
+                          {disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
                         </Button>
                         </Button>
                       }
                       }
                     />
                     />