This commit is contained in:
2026-08-15 02:35:23 +08:00
parent bd5dbf2522
commit a34133cce5
49 changed files with 929 additions and 504 deletions
+158 -4
View File
@@ -3,6 +3,10 @@ package biz
import (
"context"
"errors"
"strconv"
"strings"
"sync"
"time"
)
type SecurityConfig struct {
@@ -33,14 +37,54 @@ type SecurityRepo interface {
SaveSecurityConfig(context.Context, *SecurityConfig) error
}
type SecurityUsecase struct{ SecurityRepo }
type SecurityUsecase struct {
repo SecurityRepo
cache Cache
settings RuntimeSettings
tokens *TokenUsecase
mu sync.RWMutex
cachedConfig *SecurityConfig
}
func NewSecurityUsecase(repo SecurityRepo) *SecurityUsecase {
return &SecurityUsecase{SecurityRepo: repo}
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 {
return uc.SaveSecurityConfig(ctx, value)
if err := uc.repo.SaveSecurityConfig(ctx, value); err != nil {
return err
}
uc.mu.Lock()
copy := *value
uc.cachedConfig = &copy
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 &copy, nil
}
uc.mu.RUnlock()
value, err := uc.repo.SecurityConfig(ctx)
if err != nil {
return nil, err
}
uc.mu.Lock()
copy := *value
uc.cachedConfig = &copy
uc.mu.Unlock()
return value, nil
}
func (uc *SecurityUsecase) ValidatePassword(value *SecurityConfig, password string) error {
@@ -74,3 +118,113 @@ func (uc *SecurityUsecase) ValidatePassword(value *SecurityConfig, password stri
}
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()
}