Files
backend/internal/service/verification_service.go
lan 13bab28926
Some checks failed
SonarQube Analysis / sonarqube (push) Has been cancelled
feat: 增加登录和验证码验证失败次数限制,添加账号锁定机制
2025-12-02 10:38:25 +08:00

157 lines
4.8 KiB
Go

package service
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"time"
"carrotskin/pkg/config"
"carrotskin/pkg/email"
"carrotskin/pkg/redis"
)
const (
// 验证码类型
VerificationTypeRegister = "register"
VerificationTypeResetPassword = "reset_password"
VerificationTypeChangeEmail = "change_email"
// 验证码配置
CodeLength = 6 // 验证码长度
CodeExpiration = 10 * time.Minute // 验证码有效期
CodeRateLimit = 1 * time.Minute // 发送频率限制
)
// GenerateVerificationCode 生成6位数字验证码
func GenerateVerificationCode() (string, error) {
const digits = "0123456789"
code := make([]byte, CodeLength)
for i := range code {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits))))
if err != nil {
return "", err
}
code[i] = digits[num.Int64()]
}
return string(code), nil
}
// SendVerificationCode 发送验证码
func SendVerificationCode(ctx context.Context, redisClient *redis.Client, emailService *email.Service, email, codeType string) error {
// 测试环境下直接跳过,不存储也不发送
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
return nil
}
// 检查发送频率限制
rateLimitKey := fmt.Sprintf("verification:rate_limit:%s:%s", codeType, email)
exists, err := redisClient.Exists(ctx, rateLimitKey)
if err != nil {
return fmt.Errorf("检查发送频率失败: %w", err)
}
if exists > 0 {
return fmt.Errorf("发送过于频繁,请稍后再试")
}
// 生成验证码
code, err := GenerateVerificationCode()
if err != nil {
return fmt.Errorf("生成验证码失败: %w", err)
}
// 存储验证码到Redis
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
if err := redisClient.Set(ctx, codeKey, code, CodeExpiration); err != nil {
return fmt.Errorf("存储验证码失败: %w", err)
}
// 设置发送频率限制
if err := redisClient.Set(ctx, rateLimitKey, "1", CodeRateLimit); err != nil {
return fmt.Errorf("设置发送频率限制失败: %w", err)
}
// 发送邮件
if err := sendVerificationEmail(emailService, email, code, codeType); err != nil {
// 发送失败,删除验证码
_ = redisClient.Del(ctx, codeKey)
return fmt.Errorf("发送邮件失败: %w", err)
}
return nil
}
// VerifyCode 验证验证码
func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, codeType string) error {
// 测试环境下直接通过验证
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
return nil
}
// 检查是否被锁定
locked, ttl, err := CheckVerifyLocked(ctx, redisClient, email, codeType)
if err == nil && locked {
return fmt.Errorf("验证码错误次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1)
}
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
// 从Redis获取验证码
storedCode, err := redisClient.Get(ctx, codeKey)
if err != nil {
// 记录失败尝试并检查是否触发锁定
count, _ := RecordVerifyFailure(ctx, redisClient, email, codeType)
if count >= MaxVerifyAttempts {
return fmt.Errorf("验证码错误次数过多,账号已被锁定 %d 分钟", int(VerifyLockDuration.Minutes()))
}
remaining := MaxVerifyAttempts - count
if remaining > 0 {
return fmt.Errorf("验证码已过期或不存在,还剩 %d 次尝试机会", remaining)
}
return fmt.Errorf("验证码已过期或不存在")
}
// 验证验证码
if storedCode != code {
// 记录失败尝试并检查是否触发锁定
count, _ := RecordVerifyFailure(ctx, redisClient, email, codeType)
if count >= MaxVerifyAttempts {
return fmt.Errorf("验证码错误次数过多,账号已被锁定 %d 分钟", int(VerifyLockDuration.Minutes()))
}
remaining := MaxVerifyAttempts - count
if remaining > 0 {
return fmt.Errorf("验证码错误,还剩 %d 次尝试机会", remaining)
}
return fmt.Errorf("验证码错误")
}
// 验证成功,删除验证码和失败计数
_ = redisClient.Del(ctx, codeKey)
_ = ClearVerifyAttempts(ctx, redisClient, email, codeType)
return nil
}
// DeleteVerificationCode 删除验证码
func DeleteVerificationCode(ctx context.Context, redisClient *redis.Client, email, codeType string) error {
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
return redisClient.Del(ctx, codeKey)
}
// sendVerificationEmail 根据类型发送邮件
func sendVerificationEmail(emailService *email.Service, to, code, codeType string) error {
switch codeType {
case VerificationTypeRegister:
return emailService.SendEmailVerification(to, code)
case VerificationTypeResetPassword:
return emailService.SendResetPassword(to, code)
case VerificationTypeChangeEmail:
return emailService.SendChangeEmail(to, code)
default:
return emailService.SendVerificationCode(to, code, codeType)
}
}