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:" ) // CheckLoginLocked 检查账号是否被锁定 func CheckLoginLocked(ctx context.Context, redisClient *redis.Client, identifier string) (bool, time.Duration, error) { key := LoginLockedKeyPrefix + identifier ttl, err := redisClient.TTL(ctx, key) if err != nil { return false, 0, err } if ttl > 0 { return true, ttl, nil } return false, 0, nil } // RecordLoginFailure 记录登录失败 func RecordLoginFailure(ctx context.Context, redisClient *redis.Client, identifier string) (int, error) { attemptKey := LoginAttemptKeyPrefix + identifier // 增加失败次数 count, err := redisClient.Incr(ctx, attemptKey) if err != nil { return 0, fmt.Errorf("记录登录失败次数失败: %w", err) } // 设置过期时间(仅在第一次设置) if count == 1 { if err := redisClient.Expire(ctx, attemptKey, LoginAttemptWindow); err != nil { return int(count), fmt.Errorf("设置过期时间失败: %w", err) } } // 如果超过最大次数,锁定账号 if count >= MaxLoginAttempts { lockedKey := LoginLockedKeyPrefix + identifier if err := redisClient.Set(ctx, lockedKey, "1", LoginLockDuration); err != nil { return int(count), fmt.Errorf("锁定账号失败: %w", err) } // 清除失败计数 _ = redisClient.Del(ctx, attemptKey) } return int(count), nil } // ClearLoginAttempts 清除登录失败记录(登录成功后调用) func ClearLoginAttempts(ctx context.Context, redisClient *redis.Client, identifier string) error { attemptKey := LoginAttemptKeyPrefix + identifier return redisClient.Del(ctx, attemptKey) } // GetRemainingLoginAttempts 获取剩余登录尝试次数 func GetRemainingLoginAttempts(ctx context.Context, redisClient *redis.Client, identifier string) (int, error) { attemptKey := LoginAttemptKeyPrefix + identifier countStr, err := redisClient.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 CheckVerifyLocked(ctx context.Context, redisClient *redis.Client, email, codeType string) (bool, time.Duration, error) { key := VerifyLockedKeyPrefix + codeType + ":" + email ttl, err := redisClient.TTL(ctx, key) if err != nil { return false, 0, err } if ttl > 0 { return true, ttl, nil } return false, 0, nil } // RecordVerifyFailure 记录验证码验证失败 func RecordVerifyFailure(ctx context.Context, redisClient *redis.Client, email, codeType string) (int, error) { attemptKey := VerifyAttemptKeyPrefix + codeType + ":" + email // 增加失败次数 count, err := redisClient.Incr(ctx, attemptKey) if err != nil { return 0, fmt.Errorf("记录验证码失败次数失败: %w", err) } // 设置过期时间 if count == 1 { if err := redisClient.Expire(ctx, attemptKey, VerifyLockDuration); err != nil { return int(count), err } } // 如果超过最大次数,锁定验证 if count >= MaxVerifyAttempts { lockedKey := VerifyLockedKeyPrefix + codeType + ":" + email if err := redisClient.Set(ctx, lockedKey, "1", VerifyLockDuration); err != nil { return int(count), err } _ = redisClient.Del(ctx, attemptKey) } return int(count), nil } // ClearVerifyAttempts 清除验证码失败记录(验证成功后调用) func ClearVerifyAttempts(ctx context.Context, redisClient *redis.Client, email, codeType string) error { attemptKey := VerifyAttemptKeyPrefix + codeType + ":" + email return redisClient.Del(ctx, attemptKey) }