Files
backend/internal/service/security_service.go
lan 034e02e93a feat: Enhance dependency injection and service integration
- Updated main.go to initialize email service and include it in the dependency injection container.
- Refactored handlers to utilize context in service method calls, improving consistency and error handling.
- Introduced new service options for upload, security, and captcha services, enhancing modularity and testability.
- Removed unused repository implementations to streamline the codebase.

This commit continues the effort to improve the architecture by ensuring all services are properly injected and utilized across the application.
2025-12-02 22:52:33 +08:00

185 lines
5.7 KiB
Go

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)
}