Browse Source

验证码库

runningwater 8 tháng trước cách đây
mục cha
commit
5cf31f4a89

+ 22 - 0
config/verifycode.go

@@ -0,0 +1,22 @@
+package config
+
+import "github.com/runningwater/gohub/pkg/config"
+
+func init() {
+	config.Add("verifycode", func() map[string]any {
+		return map[string]any{
+
+			"code_length": config.Env("VERIFY_CODE_LENGTH", 6),
+			"expire_time": config.Env("VERIFY_EXPIRE_TIME", 15), // 验证码过期时间,单位分钟
+
+			// Debug 模式下的验证码过期时间,单位分钟
+			"debug_expire_time": config.Env("VERIFY_DEBUG_EXPIRE_TIME", 10080),
+			// 本地开发环境验证码使用
+			"debug_code": 12345,
+
+			// 方便本地和 测试环境调试
+			"debug_phone_prefix": "000",
+			"debug_email_prefix": "test@",
+		}
+	})
+}

+ 7 - 12
main.go

@@ -1,14 +1,14 @@
 package main
 
 import (
-    "flag"
-    "fmt"
+	"flag"
+	"fmt"
 
-    "github.com/gin-gonic/gin"
+	"github.com/gin-gonic/gin"
 
-    "github.com/runningwater/gohub/bootstrap"
-    appConfig "github.com/runningwater/gohub/config"
-    "github.com/runningwater/gohub/pkg/config"
+	"github.com/runningwater/gohub/bootstrap"
+	appConfig "github.com/runningwater/gohub/config"
+	"github.com/runningwater/gohub/pkg/config"
 )
 
 func init() {
@@ -43,12 +43,7 @@ func main() {
     bootstrap.SetupRoute(r)
 
     // 测试发送短信
-    // sms.NewSMS().Send("15968875425", sms.Message{
-    //     Template: config.GetString("sms.aliyun.template_code"),
-    //     Data: map[string]string{
-    //         "code": "123456",
-    //     },
-    // })
+    // verifycode.NewVerifyCode().SendSMS("13800138000")
 
     // 启动 HTTP 服务,监听在我们指定的端口上
     err := r.Run(":" + config.Get("app.port"))

+ 17 - 0
pkg/helpers/helpers.go

@@ -2,7 +2,9 @@
 package helpers
 
 import (
+	"crypto/rand"
 	"fmt"
+	"io"
 	"reflect"
 	"time"
 )
@@ -37,3 +39,18 @@ func Empty(val any) bool {
 func MicrosecondsStr(elapsed time.Duration) string {
 	return fmt.Sprintf("%.3fms", float64(elapsed.Nanoseconds())/1e6)
 }
+
+// 生成随机数字符串
+// length: 字符串长度
+func RandomNumber(length int) string {
+	table := [...]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}
+	b := make([]byte, length)
+	n, err := io.ReadAtLeast(rand.Reader, b, length)
+	if n != length {
+		panic(err)
+	}
+	for i := 0; i < len(b); i++ {
+		b[i] = table[int(b[i])%len(table)]
+	}
+	return string(b)
+}

+ 10 - 0
pkg/verifycode/store_interface.go

@@ -0,0 +1,10 @@
+package verifycode
+
+type Store interface {
+	// 保存验证码
+	Set(key string, value string) bool
+	// 获取验证码
+	Get(key string, clear bool) string
+	// 检查验证码是否正确
+	Verify(key, answer string, clear bool) bool
+}

+ 54 - 0
pkg/verifycode/store_reids.go

@@ -0,0 +1,54 @@
+package verifycode
+
+import (
+	"time"
+
+	"github.com/runningwater/gohub/pkg/app"
+	"github.com/runningwater/gohub/pkg/config"
+	"github.com/runningwater/gohub/pkg/redis"
+)
+
+// RedisStore 实现了 verifycode.Store 接口
+// 验证码存储在 Redis 中
+// 键名格式为:keyPrefix:code
+type RedisStore struct {
+	RedisClient *redis.RedisClient
+	KeyPrefix   string
+}
+
+// 保存验证码
+func (s *RedisStore) Set(key string, value string) bool {
+	expirTime := time.Minute * time.Duration(config.GetInt64("verifycode.expire_time"))
+	// 本地环境方便调试
+	if app.IsLocal() {
+		expirTime = time.Minute * time.Duration(config.GetInt64("verifycode.debug_expire_time"))
+	}
+	// 生成 Redis 键名
+	redisKey := s.KeyPrefix + key
+	// 设置 Redis 键值
+	return s.RedisClient.Set(redisKey, value, expirTime)
+}
+
+// 获取验证码
+func (s *RedisStore) Get(key string, clear bool) string {
+	key = s.KeyPrefix + key
+	val := s.RedisClient.Get(key)
+	if clear {
+		s.RedisClient.Del(key)
+	}
+	return val
+}
+
+// 检查验证码是否正确
+func (s *RedisStore) Verify(key, answer string, clear bool) bool {
+	v := s.Get(key, clear)
+	return v == answer
+}
+
+func NewRedisStore() *RedisStore {
+	return &RedisStore{
+		RedisClient: redis.Redis,
+		KeyPrefix:   config.GetString("app.name") + ":verifycode:",
+	}
+
+}

+ 78 - 0
pkg/verifycode/verifycode.go

@@ -0,0 +1,78 @@
+package verifycode
+
+import (
+	"strings"
+	"sync"
+
+	"github.com/runningwater/gohub/pkg/app"
+	"github.com/runningwater/gohub/pkg/config"
+	"github.com/runningwater/gohub/pkg/helpers"
+	"github.com/runningwater/gohub/pkg/logger"
+	"github.com/runningwater/gohub/pkg/sms"
+)
+
+type VerifyCode struct {
+	Store Store
+}
+
+var once sync.Once
+var internalVerifyCode *VerifyCode
+
+func NewVerifyCode() *VerifyCode {
+	once.Do(func() {
+		internalVerifyCode = &VerifyCode{
+			Store: NewRedisStore(),
+		}
+	})
+	return internalVerifyCode
+}
+
+// SendSMS 发送验证码
+// 发送验证码到手机, 调用示例
+//
+//	verifycode.NewVerifyCode().SendSMS(request.Phone)
+func (v *VerifyCode) SendSMS(phone string) bool {
+	// 生成验证码
+	code := v.generateVerifyCode(phone)
+
+	// 方便本地和 测试环境调试
+	if !app.IsProduction() &&
+		strings.HasPrefix(phone, config.GetString("verifycode.debug_phone_prefix")) {
+		// 测试环境,直接返回成功
+		return true
+	}
+	// 发送验证码
+	return sms.NewSMS().Send(phone, sms.Message{
+		Template: config.GetString("sms.aliyun.template_code"),
+		Data:     map[string]string{"code": code},
+	})
+}
+
+// CheckAnswer 检查验证码是否正确
+// key: 手机号 或 Email
+func (v *VerifyCode) CheckAnswer(key, answer string) bool {
+
+	logger.DebugJSON("验证码", "检查验证码是否正确", map[string]string{key: answer})
+	// 方便本地和 测试环境调试
+	if !app.IsProduction() &&
+		(strings.HasPrefix(key, config.GetString("verifycode.debug_phone_prefix")) || strings.HasPrefix(key, config.GetString("verifycode.debug_email_prefix"))) {
+		return true
+	}
+	return v.Store.Verify(key, answer, false)
+}
+
+// 生成验证码, 并放置于 Redis 中
+func (v *VerifyCode) generateVerifyCode(key string) string {
+	// 生成随机码
+	code := helpers.RandomNumber(config.GetInt("verifycode.code_length"))
+
+	if app.IsLocal() {
+		code = config.GetString("verifycode.debug_code")
+	}
+
+	logger.DebugJSON("验证码", "生成验证码", map[string]string{key: code})
+
+	// 存储验证码到 Redis
+	v.Store.Set(key, code)
+	return code
+}