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