mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-03 15:56:22 +00:00
⚡ implement email verification feature, add captcha validation middleware, and enhance user authentication flow
This commit is contained in:
@ -1,10 +1,22 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
ModeDev = "dev"
|
||||
ModeProd = "prod"
|
||||
RoleUser = "user"
|
||||
RoleAdmin = "admin"
|
||||
CaptchaTypeDisable = "disable" // 禁用验证码
|
||||
CaptchaTypeHCaptcha = "hcaptcha" // HCaptcha验证码
|
||||
CaptchaTypeTurnstile = "turnstile" // Turnstile验证码
|
||||
CaptchaTypeReCaptcha = "recaptcha" // ReCaptcha验证码
|
||||
ModeDev = "dev"
|
||||
ModeProd = "prod"
|
||||
RoleUser = "user"
|
||||
RoleAdmin = "admin"
|
||||
|
||||
EnvVarPasswordSalt = "PASSWORD_SALT" // 环境变量:密码盐
|
||||
EnvKeyMode = "MODE" // 环境变量:运行模式
|
||||
EnvKeyJwtSecrete = "JWT_SECRET" // 环境变量:JWT密钥
|
||||
EnvKeyPasswordSalt = "PASSWORD_SALT" // 环境变量:密码盐
|
||||
EnvKeyTokenDuration = "TOKEN_DURATION" // 环境变量:令牌有效期
|
||||
EnvKeyTokenDurationDefault = 300
|
||||
EnvKeyRefreshTokenDuration = "REFRESH_TOKEN_DURATION" // 环境变量:刷新令牌有效期
|
||||
EnvKeyRefreshTokenDurationWithRemember = "REFRESH_TOKEN_DURATION_WITH_REMEMBER" // 环境变量:记住我刷新令牌有效期
|
||||
|
||||
KVKeyEmailVerificationCode = "email_verification_code" // KV存储:邮箱验证码
|
||||
)
|
||||
|
58
pkg/errs/errors.go
Normal file
58
pkg/errs/errors.go
Normal file
@ -0,0 +1,58 @@
|
||||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ServiceError 业务错误结构
|
||||
type ServiceError struct {
|
||||
Code int // 错误代码
|
||||
Message string // 错误消息
|
||||
Err error // 原始错误
|
||||
}
|
||||
|
||||
func (e *ServiceError) Error() string {
|
||||
if e.Err != nil {
|
||||
return e.Message + ": " + e.Err.Error()
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// 常见业务错误
|
||||
var (
|
||||
ErrNotFound = &ServiceError{Code: http.StatusNotFound, Message: "not found"}
|
||||
ErrInvalidCredentials = &ServiceError{Code: http.StatusUnauthorized, Message: "invalid credentials"}
|
||||
ErrInternalServer = &ServiceError{Code: http.StatusInternalServerError, Message: "internal server error"}
|
||||
ErrBadRequest = &ServiceError{Code: http.StatusBadRequest, Message: "invalid request parameters"}
|
||||
ErrForbidden = &ServiceError{Code: http.StatusForbidden, Message: "access forbidden"}
|
||||
)
|
||||
|
||||
// New 创建自定义错误
|
||||
func New(code int, message string, err error) *ServiceError {
|
||||
return &ServiceError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Is 判断错误类型
|
||||
func Is(err, target error) bool {
|
||||
return errors.Is(err, target)
|
||||
}
|
||||
|
||||
// AsServiceError 将错误转换为ServiceError
|
||||
func AsServiceError(err error) *ServiceError {
|
||||
var serviceErr *ServiceError
|
||||
if errors.As(err, &serviceErr) {
|
||||
return serviceErr
|
||||
}
|
||||
return ErrInternalServer
|
||||
}
|
||||
|
||||
// HandleError 处理错误并返回HTTP状态码和消息
|
||||
func HandleError(c *app.RequestContext, err *ServiceError) {
|
||||
|
||||
}
|
19
pkg/errs/errors_test.go
Normal file
19
pkg/errs/errors_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
package errs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAsServiceError(c) {
|
||||
serviceError := ErrNotFound
|
||||
err := AsServiceError(serviceError)
|
||||
if err.Code != serviceError.Code || err.Message != serviceError.Message {
|
||||
t.Errorf("Expected %v, got %v", serviceError, err)
|
||||
}
|
||||
|
||||
serviceError = New(520, "Custom error", nil)
|
||||
err = AsServiceError(serviceError)
|
||||
if err.Code != serviceError.Code || err.Message != serviceError.Message {
|
||||
t.Errorf("Expected %v, got %v", serviceError, err)
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
package resps
|
||||
|
||||
const (
|
||||
ErrParamInvalid = "invalid request parameters"
|
||||
ErrUnauthorized = "unauthorized access"
|
||||
ErrForbidden = "access forbidden"
|
||||
ErrNotFound = "resource not found"
|
||||
Success = "success"
|
||||
ErrParamInvalid = "invalid request parameters"
|
||||
ErrUnauthorized = "unauthorized access"
|
||||
ErrForbidden = "access forbidden"
|
||||
ErrNotFound = "resource not found"
|
||||
ErrInternalServerError = "internal server error"
|
||||
|
||||
ErrInvalidCredentials = "invalid credentials"
|
||||
)
|
||||
|
93
pkg/utils/captcha.go
Normal file
93
pkg/utils/captcha.go
Normal file
@ -0,0 +1,93 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type captchaUtils struct{}
|
||||
|
||||
var Captcha = captchaUtils{}
|
||||
|
||||
type CaptchaConfig struct {
|
||||
Type string
|
||||
SiteSecret string // Site secret key for the captcha service
|
||||
SecretKey string // Secret key for the captcha service
|
||||
}
|
||||
|
||||
func (c *captchaUtils) GetCaptchaConfigFromEnv() *CaptchaConfig {
|
||||
return &CaptchaConfig{
|
||||
Type: Env.Get("CAPTCHA_TYPE", "disable"),
|
||||
SiteSecret: Env.Get("CAPTCHA_SITE_SECRET", ""),
|
||||
SecretKey: Env.Get("CAPTCHA_SECRET_KEY", ""),
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyCaptcha 根据提供的配置和令牌验证验证码
|
||||
func (c *captchaUtils) VerifyCaptcha(captchaConfig *CaptchaConfig, captchaToken string) (bool, error) {
|
||||
restyClient := resty.New()
|
||||
switch captchaConfig.Type {
|
||||
case constant.CaptchaTypeDisable:
|
||||
return true, nil
|
||||
case constant.CaptchaTypeHCaptcha:
|
||||
result := make(map[string]any)
|
||||
resp, err := restyClient.R().
|
||||
SetFormData(map[string]string{
|
||||
"secret": captchaConfig.SecretKey,
|
||||
"response": captchaToken,
|
||||
}).SetResult(&result).Post("https://hcaptcha.com/siteverify")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if resp.IsError() {
|
||||
return false, nil
|
||||
}
|
||||
fmt.Printf("%#v\n", result)
|
||||
if success, ok := result["success"].(bool); ok && success {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
case constant.CaptchaTypeTurnstile:
|
||||
result := make(map[string]any)
|
||||
resp, err := restyClient.R().
|
||||
SetFormData(map[string]string{
|
||||
"secret": captchaConfig.SecretKey,
|
||||
"response": captchaToken,
|
||||
}).SetResult(&result).Post("https://challenges.cloudflare.com/turnstile/v0/siteverify")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if resp.IsError() {
|
||||
return false, nil
|
||||
}
|
||||
fmt.Printf("%#v\n", result)
|
||||
if success, ok := result["success"].(bool); ok && success {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
case constant.CaptchaTypeReCaptcha:
|
||||
result := make(map[string]any)
|
||||
resp, err := restyClient.R().
|
||||
SetFormData(map[string]string{
|
||||
"secret": captchaConfig.SecretKey,
|
||||
"response": captchaToken,
|
||||
}).SetResult(&result).Post("https://www.google.com/recaptcha/api/siteverify")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if resp.IsError() {
|
||||
return false, nil
|
||||
}
|
||||
fmt.Printf("%#v\n", result)
|
||||
if success, ok := result["success"].(bool); ok && success {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
default:
|
||||
return false, fmt.Errorf("invalid captcha type: %s", captchaConfig.Type)
|
||||
}
|
||||
}
|
84
pkg/utils/email.go
Normal file
84
pkg/utils/email.go
Normal file
@ -0,0 +1,84 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"gopkg.in/gomail.v2"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
type emailUtils struct{}
|
||||
|
||||
var Email = emailUtils{}
|
||||
|
||||
type EmailConfig struct {
|
||||
Enable bool // 邮箱启用状态
|
||||
Username string // 邮箱用户名
|
||||
Address string // 邮箱地址
|
||||
Host string // 邮箱服务器地址
|
||||
Port int // 邮箱服务器端口
|
||||
Password string // 邮箱密码
|
||||
SSL bool // 是否使用SSL
|
||||
}
|
||||
|
||||
// SendTemplate 发送HTML模板,从配置文件中读取邮箱配置,支持上下文控制
|
||||
func (e *emailUtils) SendTemplate(emailConfig *EmailConfig, target, subject, htmlTemplate string, data map[string]interface{}) error {
|
||||
// 使用Go的模板系统处理HTML模板
|
||||
tmpl, err := template.New("email").Parse(htmlTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析模板失败: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return fmt.Errorf("执行模板失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送处理后的HTML内容
|
||||
return e.SendEmail(emailConfig, target, subject, buf.String(), true)
|
||||
}
|
||||
|
||||
// SendEmail 使用gomail库发送邮件
|
||||
func (e *emailUtils) SendEmail(emailConfig *EmailConfig, target, subject, content string, isHTML bool) error {
|
||||
if !emailConfig.Enable {
|
||||
return nil
|
||||
}
|
||||
// 创建新邮件
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", emailConfig.Address)
|
||||
m.SetHeader("To", target)
|
||||
m.SetHeader("Subject", subject)
|
||||
// 设置内容类型
|
||||
if isHTML {
|
||||
m.SetBody("text/html", content)
|
||||
} else {
|
||||
m.SetBody("text/plain", content)
|
||||
}
|
||||
// 创建发送器
|
||||
d := gomail.NewDialer(emailConfig.Host, emailConfig.Port, emailConfig.Username, emailConfig.Password)
|
||||
// 配置SSL/TLS
|
||||
if emailConfig.SSL {
|
||||
d.SSL = true
|
||||
} else {
|
||||
// 对于非SSL但需要STARTTLS的情况
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
// 发送邮件
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
return fmt.Errorf("发送邮件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *emailUtils) GetEmailConfigFromEnv() *EmailConfig {
|
||||
return &EmailConfig{
|
||||
Enable: Env.GetenvAsBool("EMAIL_ENABLE", false),
|
||||
Username: Env.Get("EMAIL_USERNAME", ""),
|
||||
Address: Env.Get("EMAIL_ADDRESS", ""),
|
||||
Host: Env.Get("EMAIL_HOST", "smtp.example.com"),
|
||||
Port: Env.GetenvAsInt("EMAIL_PORT", 587),
|
||||
Password: Env.Get("EMAIL_PASSWORD", ""),
|
||||
SSL: Env.GetenvAsBool("EMAIL_SSL", true),
|
||||
}
|
||||
}
|
@ -2,19 +2,27 @@ package utils
|
||||
|
||||
import (
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
IsDevMode = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
_ = godotenv.Load()
|
||||
|
||||
// Init env
|
||||
IsDevMode = Env.Get(constant.EnvKeyMode, constant.ModeDev) == constant.ModeDev
|
||||
}
|
||||
|
||||
type envType struct{}
|
||||
type envUtils struct{}
|
||||
|
||||
var Env envType
|
||||
var Env envUtils
|
||||
|
||||
func (e *envType) Get(key string, defaultValue ...string) string {
|
||||
func (e *envUtils) Get(key string, defaultValue ...string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" && len(defaultValue) > 0 {
|
||||
return defaultValue[0]
|
||||
@ -22,7 +30,7 @@ func (e *envType) Get(key string, defaultValue ...string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
func (e *envType) GetenvAsInt(key string, defaultValue ...int) int {
|
||||
func (e *envUtils) GetenvAsInt(key string, defaultValue ...int) int {
|
||||
value := os.Getenv(key)
|
||||
if value == "" && len(defaultValue) > 0 {
|
||||
return defaultValue[0]
|
||||
@ -34,7 +42,7 @@ func (e *envType) GetenvAsInt(key string, defaultValue ...int) int {
|
||||
return intValue
|
||||
}
|
||||
|
||||
func (e *envType) GetenvAsBool(key string, defaultValue ...bool) bool {
|
||||
func (e *envUtils) GetenvAsBool(key string, defaultValue ...bool) bool {
|
||||
value := os.Getenv(key)
|
||||
if value == "" && len(defaultValue) > 0 {
|
||||
return defaultValue[0]
|
||||
|
52
pkg/utils/json_web_token.go
Normal file
52
pkg/utils/json_web_token.go
Normal file
@ -0,0 +1,52 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"time"
|
||||
)
|
||||
|
||||
type jwtUtils struct{}
|
||||
|
||||
var Jwt = jwtUtils{}
|
||||
|
||||
type Claims struct {
|
||||
jwt.RegisteredClaims
|
||||
UserID uint `json:"user_id"`
|
||||
SessionKey string `json:"session_key"` // 会话ID,仅在有状态Token中使用
|
||||
Stateful bool `json:"stateful"` // 是否为有状态Token
|
||||
}
|
||||
|
||||
// NewClaims 创建一个新的Claims实例,对于无状态
|
||||
func (j *jwtUtils) NewClaims(userID uint, sessionKey string, stateful bool, duration time.Duration) *Claims {
|
||||
return &Claims{
|
||||
UserID: userID,
|
||||
SessionKey: sessionKey,
|
||||
Stateful: stateful,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ToString 将Claims转换为JWT字符串
|
||||
func (c *Claims) ToString() (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
|
||||
return token.SignedString([]byte(Env.Get(constant.EnvKeyJwtSecrete, "default_jwt_secret")))
|
||||
}
|
||||
|
||||
// ParseJsonWebTokenWithoutState 解析JWT令牌,不对有状态的Token进行状态检查
|
||||
func (j *jwtUtils) ParseJsonWebTokenWithoutState(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (any, error) {
|
||||
return []byte(Env.Get(constant.EnvKeyJwtSecrete, "default_jwt_secret")), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
return claims, nil
|
||||
}
|
119
pkg/utils/kvstore.go
Normal file
119
pkg/utils/kvstore.go
Normal file
@ -0,0 +1,119 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type kvStoreUtils struct{}
|
||||
|
||||
var KV kvStoreUtils
|
||||
|
||||
// KVStore 是一个简单的内存键值存储系统
|
||||
type KVStore struct {
|
||||
data map[string]storeItem
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// storeItem 代表存储的单个数据项
|
||||
type storeItem struct {
|
||||
value interface{}
|
||||
expiration int64 // Unix时间戳,0表示永不过期
|
||||
}
|
||||
|
||||
// 全局单例
|
||||
var (
|
||||
kvStore *KVStore
|
||||
kvStoreOnce sync.Once
|
||||
)
|
||||
|
||||
// GetInstance 获取KVStore单例实例
|
||||
func (kv *kvStoreUtils) GetInstance() *KVStore {
|
||||
kvStoreOnce.Do(func() {
|
||||
kvStore = &KVStore{
|
||||
data: make(map[string]storeItem),
|
||||
}
|
||||
// 启动清理过期项的协程
|
||||
go kvStore.startCleanupTimer()
|
||||
})
|
||||
return kvStore
|
||||
}
|
||||
|
||||
// Set 设置键值对,可选指定过期时间
|
||||
func (s *KVStore) Set(key string, value interface{}, ttl time.Duration) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
var expiration int64
|
||||
if ttl > 0 {
|
||||
expiration = time.Now().Add(ttl).Unix()
|
||||
}
|
||||
|
||||
s.data[key] = storeItem{
|
||||
value: value,
|
||||
expiration: expiration,
|
||||
}
|
||||
}
|
||||
|
||||
// Get 获取键对应的值,如果键不存在或已过期则返回(nil, false)
|
||||
func (s *KVStore) Get(key string) (interface{}, bool) {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
item, exists := s.data[key]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if item.expiration > 0 && time.Now().Unix() > item.expiration {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return item.value, true
|
||||
}
|
||||
|
||||
// Delete 删除键值对
|
||||
func (s *KVStore) Delete(key string) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
delete(s.data, key)
|
||||
}
|
||||
|
||||
// Clear 清空所有键值对
|
||||
func (s *KVStore) Clear() {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.data = make(map[string]storeItem)
|
||||
}
|
||||
|
||||
// startCleanupTimer 启动定期清理过期项的计时器
|
||||
func (s *KVStore) startCleanupTimer() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
s.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup 清理过期的数据项
|
||||
func (s *KVStore) cleanup() {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
now := time.Now().Unix()
|
||||
for key, item := range s.data {
|
||||
if item.expiration > 0 && now > item.expiration {
|
||||
delete(s.data, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// secureRand 生成0到max-1之间的安全随机数
|
||||
func secureRand(max int) int {
|
||||
// 简单实现,可以根据需要使用crypto/rand替换
|
||||
return int(time.Now().UnixNano() % int64(max))
|
||||
}
|
@ -6,13 +6,13 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type PasswordType struct {
|
||||
type PasswordUtils struct {
|
||||
}
|
||||
|
||||
var Password = PasswordType{}
|
||||
var Password = PasswordUtils{}
|
||||
|
||||
// HashPassword 密码哈希函数
|
||||
func (u *PasswordType) HashPassword(password string, salt string) (string, error) {
|
||||
func (u *PasswordUtils) HashPassword(password string, salt string) (string, error) {
|
||||
saltedPassword := Password.addSalt(password, salt)
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(saltedPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@ -22,7 +22,7 @@ func (u *PasswordType) HashPassword(password string, salt string) (string, error
|
||||
}
|
||||
|
||||
// VerifyPassword 验证密码
|
||||
func (u *PasswordType) VerifyPassword(password, hashedPassword string, salt string) bool {
|
||||
func (u *PasswordUtils) VerifyPassword(password, hashedPassword string, salt string) bool {
|
||||
if len(hashedPassword) == 0 || len(salt) == 0 {
|
||||
// 防止oidc空密码出问题
|
||||
return false
|
||||
@ -33,7 +33,7 @@ func (u *PasswordType) VerifyPassword(password, hashedPassword string, salt stri
|
||||
}
|
||||
|
||||
// addSalt 加盐函数
|
||||
func (u *PasswordType) addSalt(password string, salt string) string {
|
||||
func (u *PasswordUtils) addSalt(password string, salt string) string {
|
||||
combined := password + salt
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(combined))
|
||||
|
1
pkg/utils/request_context.go
Normal file
1
pkg/utils/request_context.go
Normal file
@ -0,0 +1 @@
|
||||
package utils
|
27
pkg/utils/strings.go
Normal file
27
pkg/utils/strings.go
Normal file
@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import "math/rand"
|
||||
|
||||
type stringsUtils struct{}
|
||||
|
||||
var Strings = stringsUtils{}
|
||||
|
||||
func (s *stringsUtils) GenerateRandomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
result[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func (s *stringsUtils) GenerateRandomStringWithCharset(length int, charset string) string {
|
||||
if charset == "" {
|
||||
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
}
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
result[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
Reference in New Issue
Block a user