2025-12-02 10:33:19 +08:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"carrotskin/pkg/redis"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// 登录失败限制配置
|
2025-12-02 22:52:33 +08:00
|
|
|
MaxLoginAttempts = 5 // 最大登录失败次数
|
|
|
|
|
LoginLockDuration = 15 * time.Minute // 账号锁定时间
|
|
|
|
|
LoginAttemptWindow = 10 * time.Minute // 失败次数统计窗口
|
2025-12-02 10:33:19 +08:00
|
|
|
|
|
|
|
|
// 验证码错误限制配置
|
2025-12-02 22:52:33 +08:00
|
|
|
MaxVerifyAttempts = 5 // 最大验证码错误次数
|
|
|
|
|
VerifyLockDuration = 30 * time.Minute // 验证码锁定时间
|
2025-12-02 10:33:19 +08:00
|
|
|
|
|
|
|
|
// Redis Key 前缀
|
|
|
|
|
LoginAttemptKeyPrefix = "security:login_attempt:"
|
|
|
|
|
LoginLockedKeyPrefix = "security:login_locked:"
|
|
|
|
|
VerifyAttemptKeyPrefix = "security:verify_attempt:"
|
|
|
|
|
VerifyLockedKeyPrefix = "security:verify_locked:"
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
// securityService SecurityService的实现
|
|
|
|
|
type securityService struct {
|
|
|
|
|
redis *redis.Client
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewSecurityService 创建SecurityService实例
|
|
|
|
|
func NewSecurityService(redisClient *redis.Client) SecurityService {
|
|
|
|
|
return &securityService{
|
|
|
|
|
redis: redisClient,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
// CheckLoginLocked 检查账号是否被锁定
|
2025-12-02 22:52:33 +08:00
|
|
|
func (s *securityService) CheckLoginLocked(ctx context.Context, identifier string) (bool, time.Duration, error) {
|
2025-12-02 10:33:19 +08:00
|
|
|
key := LoginLockedKeyPrefix + identifier
|
2025-12-02 22:52:33 +08:00
|
|
|
ttl, err := s.redis.TTL(ctx, key)
|
2025-12-02 10:33:19 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return false, 0, err
|
|
|
|
|
}
|
|
|
|
|
if ttl > 0 {
|
|
|
|
|
return true, ttl, nil
|
|
|
|
|
}
|
|
|
|
|
return false, 0, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecordLoginFailure 记录登录失败
|
2025-12-02 22:52:33 +08:00
|
|
|
func (s *securityService) RecordLoginFailure(ctx context.Context, identifier string) (int, error) {
|
2025-12-02 10:33:19 +08:00
|
|
|
attemptKey := LoginAttemptKeyPrefix + identifier
|
2025-12-02 22:52:33 +08:00
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
// 增加失败次数
|
2025-12-02 22:52:33 +08:00
|
|
|
count, err := s.redis.Incr(ctx, attemptKey)
|
2025-12-02 10:33:19 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("记录登录失败次数失败: %w", err)
|
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
// 设置过期时间(仅在第一次设置)
|
|
|
|
|
if count == 1 {
|
2025-12-02 22:52:33 +08:00
|
|
|
if err := s.redis.Expire(ctx, attemptKey, LoginAttemptWindow); err != nil {
|
2025-12-02 10:33:19 +08:00
|
|
|
return int(count), fmt.Errorf("设置过期时间失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
// 如果超过最大次数,锁定账号
|
|
|
|
|
if count >= MaxLoginAttempts {
|
|
|
|
|
lockedKey := LoginLockedKeyPrefix + identifier
|
2025-12-02 22:52:33 +08:00
|
|
|
if err := s.redis.Set(ctx, lockedKey, "1", LoginLockDuration); err != nil {
|
2025-12-02 10:33:19 +08:00
|
|
|
return int(count), fmt.Errorf("锁定账号失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
// 清除失败计数
|
2025-12-02 22:52:33 +08:00
|
|
|
_ = s.redis.Del(ctx, attemptKey)
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
return int(count), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ClearLoginAttempts 清除登录失败记录(登录成功后调用)
|
2025-12-02 22:52:33 +08:00
|
|
|
func (s *securityService) ClearLoginAttempts(ctx context.Context, identifier string) error {
|
2025-12-02 10:33:19 +08:00
|
|
|
attemptKey := LoginAttemptKeyPrefix + identifier
|
2025-12-02 22:52:33 +08:00
|
|
|
return s.redis.Del(ctx, attemptKey)
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetRemainingLoginAttempts 获取剩余登录尝试次数
|
2025-12-02 22:52:33 +08:00
|
|
|
func (s *securityService) GetRemainingLoginAttempts(ctx context.Context, identifier string) (int, error) {
|
2025-12-02 10:33:19 +08:00
|
|
|
attemptKey := LoginAttemptKeyPrefix + identifier
|
2025-12-02 22:52:33 +08:00
|
|
|
countStr, err := s.redis.Get(ctx, attemptKey)
|
2025-12-02 10:33:19 +08:00
|
|
|
if err != nil {
|
|
|
|
|
// key 不存在,返回最大次数
|
|
|
|
|
return MaxLoginAttempts, nil
|
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
var count int
|
|
|
|
|
fmt.Sscanf(countStr, "%d", &count)
|
|
|
|
|
remaining := MaxLoginAttempts - count
|
|
|
|
|
if remaining < 0 {
|
|
|
|
|
remaining = 0
|
|
|
|
|
}
|
|
|
|
|
return remaining, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CheckVerifyLocked 检查验证码是否被锁定
|
2025-12-02 22:52:33 +08:00
|
|
|
func (s *securityService) CheckVerifyLocked(ctx context.Context, email, codeType string) (bool, time.Duration, error) {
|
2025-12-02 10:33:19 +08:00
|
|
|
key := VerifyLockedKeyPrefix + codeType + ":" + email
|
2025-12-02 22:52:33 +08:00
|
|
|
ttl, err := s.redis.TTL(ctx, key)
|
2025-12-02 10:33:19 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return false, 0, err
|
|
|
|
|
}
|
|
|
|
|
if ttl > 0 {
|
|
|
|
|
return true, ttl, nil
|
|
|
|
|
}
|
|
|
|
|
return false, 0, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecordVerifyFailure 记录验证码验证失败
|
2025-12-02 22:52:33 +08:00
|
|
|
func (s *securityService) RecordVerifyFailure(ctx context.Context, email, codeType string) (int, error) {
|
2025-12-02 10:33:19 +08:00
|
|
|
attemptKey := VerifyAttemptKeyPrefix + codeType + ":" + email
|
2025-12-02 22:52:33 +08:00
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
// 增加失败次数
|
2025-12-02 22:52:33 +08:00
|
|
|
count, err := s.redis.Incr(ctx, attemptKey)
|
2025-12-02 10:33:19 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("记录验证码失败次数失败: %w", err)
|
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
// 设置过期时间
|
|
|
|
|
if count == 1 {
|
2025-12-02 22:52:33 +08:00
|
|
|
if err := s.redis.Expire(ctx, attemptKey, VerifyLockDuration); err != nil {
|
2025-12-02 10:33:19 +08:00
|
|
|
return int(count), err
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
// 如果超过最大次数,锁定验证
|
|
|
|
|
if count >= MaxVerifyAttempts {
|
|
|
|
|
lockedKey := VerifyLockedKeyPrefix + codeType + ":" + email
|
2025-12-02 22:52:33 +08:00
|
|
|
if err := s.redis.Set(ctx, lockedKey, "1", VerifyLockDuration); err != nil {
|
2025-12-02 10:33:19 +08:00
|
|
|
return int(count), err
|
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
_ = s.redis.Del(ctx, attemptKey)
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
2025-12-02 10:33:19 +08:00
|
|
|
return int(count), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ClearVerifyAttempts 清除验证码失败记录(验证成功后调用)
|
2025-12-02 22:52:33 +08:00
|
|
|
func (s *securityService) ClearVerifyAttempts(ctx context.Context, email, codeType string) error {
|
2025-12-02 10:33:19 +08:00
|
|
|
attemptKey := VerifyAttemptKeyPrefix + codeType + ":" + email
|
2025-12-02 22:52:33 +08:00
|
|
|
return s.redis.Del(ctx, attemptKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 全局函数,保持向后兼容,用于已存在的代码
|
|
|
|
|
func CheckLoginLocked(ctx context.Context, redisClient *redis.Client, identifier string) (bool, time.Duration, error) {
|
|
|
|
|
svc := NewSecurityService(redisClient)
|
|
|
|
|
return svc.CheckLoginLocked(ctx, identifier)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RecordLoginFailure(ctx context.Context, redisClient *redis.Client, identifier string) (int, error) {
|
|
|
|
|
svc := NewSecurityService(redisClient)
|
|
|
|
|
return svc.RecordLoginFailure(ctx, identifier)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ClearLoginAttempts(ctx context.Context, redisClient *redis.Client, identifier string) error {
|
|
|
|
|
svc := NewSecurityService(redisClient)
|
|
|
|
|
return svc.ClearLoginAttempts(ctx, identifier)
|
2025-12-02 10:33:19 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
func CheckVerifyLocked(ctx context.Context, redisClient *redis.Client, email, codeType string) (bool, time.Duration, error) {
|
|
|
|
|
svc := NewSecurityService(redisClient)
|
|
|
|
|
return svc.CheckVerifyLocked(ctx, email, codeType)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RecordVerifyFailure(ctx context.Context, redisClient *redis.Client, email, codeType string) (int, error) {
|
|
|
|
|
svc := NewSecurityService(redisClient)
|
|
|
|
|
return svc.RecordVerifyFailure(ctx, email, codeType)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ClearVerifyAttempts(ctx context.Context, redisClient *redis.Client, email, codeType string) error {
|
|
|
|
|
svc := NewSecurityService(redisClient)
|
|
|
|
|
return svc.ClearVerifyAttempts(ctx, email, codeType)
|
|
|
|
|
}
|