- 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.
185 lines
5.7 KiB
Go
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)
|
|
}
|