package biz import ( "context" "errors" "strconv" "strings" "sync" "time" ) type SecurityConfig struct { ID uint CaptchaOpen int CaptchaTimeout int KeyLong int ImgWidth int ImgHeight int PwdMinLength int PwdRequireUpper bool PwdRequireLower bool PwdRequireDigit bool PwdRequireSpecial bool LimitEnable bool LimitWindow int LimitCount int LockEnable bool LockThreshold int LockDuration int PwdExpireEnable bool PwdExpireDays int ForceNewUserChangePassword bool } type SecurityRepo interface { SecurityConfig(context.Context) (*SecurityConfig, error) SaveSecurityConfig(context.Context, *SecurityConfig) error } type SecurityUsecase struct { repo SecurityRepo cache Cache settings RuntimeSettings tokens *TokenUsecase mu sync.RWMutex cachedConfig *SecurityConfig } var ErrTokenRevoke = errors.New("token revoke failed") type TokenRevokeError struct{ Err error } func (e *TokenRevokeError) Error() string { return e.Err.Error() } func (e *TokenRevokeError) Is(target error) bool { return target == ErrTokenRevoke } func NewSecurityUsecase(repo SecurityRepo, cache Cache, settings RuntimeSettings, tokens *TokenUsecase) *SecurityUsecase { return &SecurityUsecase{repo: repo, cache: cache, settings: settings, tokens: tokens} } func (uc *SecurityUsecase) UpdateSecurity(ctx context.Context, value *SecurityConfig) error { if err := uc.repo.SaveSecurityConfig(ctx, value); err != nil { return err } uc.mu.Lock() copy := *value uc.cachedConfig = © uc.mu.Unlock() return nil } func (uc *SecurityUsecase) Current(ctx context.Context) (*SecurityConfig, error) { uc.mu.RLock() if uc.cachedConfig != nil { copy := *uc.cachedConfig uc.mu.RUnlock() return ©, nil } uc.mu.RUnlock() value, err := uc.repo.SecurityConfig(ctx) if err != nil { return nil, err } uc.mu.Lock() copy := *value uc.cachedConfig = © uc.mu.Unlock() return value, nil } func (uc *SecurityUsecase) ValidatePassword(value *SecurityConfig, password string) error { if len([]rune(password)) < value.PwdMinLength { return errors.New("密码长度不足") } hasUpper, hasLower, hasDigit, hasSpecial := false, false, false, false for _, ch := range password { switch { case ch >= 'A' && ch <= 'Z': hasUpper = true case ch >= 'a' && ch <= 'z': hasLower = true case ch >= '0' && ch <= '9': hasDigit = true default: hasSpecial = true } } if value.PwdRequireUpper && !hasUpper { return errors.New("密码必须包含大写字母") } if value.PwdRequireLower && !hasLower { return errors.New("密码必须包含小写字母") } if value.PwdRequireDigit && !hasDigit { return errors.New("密码必须包含数字") } if value.PwdRequireSpecial && !hasSpecial { return errors.New("密码必须包含特殊字符") } return nil } func (uc *SecurityUsecase) ValidateCurrentPassword(ctx context.Context, password string) error { config, err := uc.Current(ctx) if err != nil { return err } return uc.ValidatePassword(config, password) } func (uc *SecurityUsecase) LoginLocked(ctx context.Context, username string) (bool, error) { _, locked, err := uc.cache.Get(ctx, "login:lock:"+username) return locked, err } func (uc *SecurityUsecase) IncrementLoginFailure(ctx context.Context, username string, expiration time.Duration) (int64, error) { return uc.cache.Increment(ctx, "login:fail:"+username, expiration) } func (uc *SecurityUsecase) LockLogin(ctx context.Context, username string, expiration time.Duration) error { return uc.cache.Set(ctx, "login:lock:"+username, "1", expiration) } func (uc *SecurityUsecase) ClearLoginState(ctx context.Context, username string) { _ = uc.cache.Delete(ctx, "login:fail:"+username) _ = uc.cache.Delete(ctx, "login:lock:"+username) } func (uc *SecurityUsecase) EnsureLoginIPCounter(ctx context.Context, ip string, expiration time.Duration) (int, error) { value, exists, err := uc.cache.Get(ctx, ip) if err != nil { return 0, err } if exists { return strconv.Atoi(value) } if expiration <= 0 { expiration = time.Hour } if err = uc.cache.Set(ctx, ip, "1", expiration); err != nil { return 0, err } return 1, nil } func (uc *SecurityUsecase) IncrementLoginIP(ctx context.Context, ip string, expiration time.Duration) (int64, error) { return uc.cache.Increment(ctx, ip, expiration) } func (uc *SecurityUsecase) IncrementRateLimit(ctx context.Context, key string, expiration time.Duration) (int64, error) { return uc.cache.Increment(ctx, key, expiration) } func (uc *SecurityUsecase) SetCaptcha(ctx context.Context, id, value string, expiration time.Duration) error { return uc.cache.Set(ctx, "captcha:"+id, value, expiration) } func (uc *SecurityUsecase) GetCaptcha(ctx context.Context, id string) (string, bool, error) { return uc.cache.Get(ctx, "captcha:"+id) } func (uc *SecurityUsecase) DeleteCaptcha(ctx context.Context, id string) error { return uc.cache.Delete(ctx, "captcha:"+id) } func (uc *SecurityUsecase) VerifyCaptcha(ctx context.Context, id, answer string, clear bool) bool { if id == "" || answer == "" { return false } value, ok, err := uc.GetCaptcha(ctx, id) if err != nil || !ok { return false } if clear { _ = uc.DeleteCaptcha(ctx, id) } return strings.EqualFold(value, answer) } func activeTokenKey(username string) string { return "jwt:active:" + username } func (uc *SecurityUsecase) ActiveToken(ctx context.Context, username string) (string, error) { value, _, err := uc.cache.Get(ctx, activeTokenKey(username)) return value, err } func (uc *SecurityUsecase) ActiveTokenMatches(ctx context.Context, username, token string) (bool, error) { if !uc.settings.UseMultipoint() { return true, nil } active, ok, err := uc.cache.Get(ctx, activeTokenKey(username)) return ok && active == token, err } func (uc *SecurityUsecase) RotateActiveToken(ctx context.Context, username, oldToken, newToken string, expiration time.Duration) error { if !uc.settings.UseMultipoint() { return nil } if oldToken != "" && oldToken != newToken { if err := uc.tokens.BlacklistToken(ctx, oldToken); err != nil { return &TokenRevokeError{Err: err} } } return uc.cache.Set(ctx, activeTokenKey(username), newToken, expiration) } func (uc *SecurityUsecase) UseMultipoint() bool { return uc.settings.UseMultipoint() } func (uc *SecurityUsecase) CaptchaRuntimeSettings() CaptchaSettings { return uc.settings.CaptchaSettings() }