优化
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAccountLocked = errors.New("account locked")
|
||||
ErrCaptchaInvalid = errors.New("captcha invalid")
|
||||
ErrTokenIssue = errors.New("token issue failed")
|
||||
ErrLoginState = errors.New("login state failed")
|
||||
)
|
||||
|
||||
type UserDisabledError struct{ UserID uint }
|
||||
|
||||
func (e *UserDisabledError) Error() string { return ErrUserDisabled.Error() }
|
||||
func (e *UserDisabledError) Unwrap() error { return ErrUserDisabled }
|
||||
|
||||
type AccountLockedError struct{ Minutes int }
|
||||
|
||||
func (e *AccountLockedError) Error() string { return ErrAccountLocked.Error() }
|
||||
func (e *AccountLockedError) Unwrap() error { return ErrAccountLocked }
|
||||
|
||||
type LoginAttempt struct {
|
||||
Username string
|
||||
Password string
|
||||
CaptchaID string
|
||||
Captcha string
|
||||
IP string
|
||||
Agent string
|
||||
}
|
||||
|
||||
type AuthenticationResult struct {
|
||||
User *User
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
NeedChangePassword bool
|
||||
}
|
||||
|
||||
type AuthenticationUsecase struct {
|
||||
users *UserUsecase
|
||||
security *SecurityUsecase
|
||||
issuer TokenIssuer
|
||||
audit AuditRecordRepo
|
||||
}
|
||||
|
||||
func NewAuthenticationUsecase(users *UserUsecase, security *SecurityUsecase, issuer TokenIssuer, audit AuditRecordRepo) *AuthenticationUsecase {
|
||||
return &AuthenticationUsecase{users: users, security: security, issuer: issuer, audit: audit}
|
||||
}
|
||||
|
||||
func (uc *AuthenticationUsecase) recordLogin(ctx context.Context, attempt *LoginAttempt, status bool, message string, userID uint) {
|
||||
_ = uc.audit.RecordLogin(ctx, &LoginLog{Username: attempt.Username, IP: attempt.IP, Status: status, ErrorMessage: message, Agent: attempt.Agent, UserID: userID})
|
||||
}
|
||||
|
||||
func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttempt) (*AuthenticationResult, error) {
|
||||
config, _ := uc.security.Current(ctx)
|
||||
if config != nil && config.LockEnable {
|
||||
if locked, _ := uc.security.LoginLocked(ctx, attempt.Username); locked {
|
||||
uc.recordLogin(ctx, attempt, false, "账号已锁定", 0)
|
||||
return nil, &AccountLockedError{Minutes: config.LockDuration}
|
||||
}
|
||||
}
|
||||
|
||||
ipTTL := time.Hour
|
||||
requireCaptcha := config == nil || config.CaptchaOpen == 0
|
||||
if config != nil {
|
||||
if config.CaptchaTimeout > 0 {
|
||||
ipTTL = time.Duration(config.CaptchaTimeout) * time.Second
|
||||
}
|
||||
failures, _ := uc.security.EnsureLoginIPCounter(ctx, attempt.IP, ipTTL)
|
||||
requireCaptcha = config.CaptchaOpen == 0 || failures > config.CaptchaOpen
|
||||
}
|
||||
if requireCaptcha && !uc.security.VerifyCaptcha(ctx, attempt.CaptchaID, attempt.Captcha, true) {
|
||||
_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
|
||||
uc.recordLogin(ctx, attempt, false, "验证码错误", 0)
|
||||
return nil, ErrCaptchaInvalid
|
||||
}
|
||||
|
||||
user, err := uc.users.Login(ctx, attempt.Username, attempt.Password)
|
||||
if err != nil {
|
||||
_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
|
||||
if config != nil && config.LockEnable {
|
||||
lockTTL := time.Duration(config.LockDuration) * time.Minute
|
||||
failures, _ := uc.security.IncrementLoginFailure(ctx, attempt.Username, lockTTL)
|
||||
if int(failures) >= config.LockThreshold {
|
||||
_ = uc.security.LockLogin(ctx, attempt.Username, lockTTL)
|
||||
}
|
||||
}
|
||||
uc.recordLogin(ctx, attempt, false, "用户名不存在或者密码错误", 0)
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
if user.Enable != 1 {
|
||||
_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
|
||||
uc.recordLogin(ctx, attempt, false, "用户被禁止登录", user.ID)
|
||||
return nil, &UserDisabledError{UserID: user.ID}
|
||||
}
|
||||
|
||||
uc.security.ClearLoginState(ctx, attempt.Username)
|
||||
if config != nil && config.PwdExpireEnable && user.PasswordUpdatedAt != nil && time.Since(*user.PasswordUpdatedAt) > time.Duration(config.PwdExpireDays)*24*time.Hour {
|
||||
user.MustChangePassword = true
|
||||
}
|
||||
issued, err := uc.issuer.IssueToken(user, user.AuthorityID, user.MustChangePassword, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrTokenIssue, err)
|
||||
}
|
||||
if uc.security.UseMultipoint() {
|
||||
oldToken, cacheErr := uc.security.ActiveToken(ctx, user.Username)
|
||||
if cacheErr != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr)
|
||||
}
|
||||
if cacheErr = uc.security.RotateActiveToken(ctx, user.Username, oldToken, issued.Value, issued.TTL); cacheErr != nil {
|
||||
if errors.Is(cacheErr, ErrTokenRevoke) {
|
||||
return nil, cacheErr
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr)
|
||||
}
|
||||
}
|
||||
uc.recordLogin(ctx, attempt, true, "登录成功", user.ID)
|
||||
return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: user.MustChangePassword}, nil
|
||||
}
|
||||
|
||||
func (uc *AuthenticationUsecase) SwitchAuthority(ctx context.Context, userID, authorityID uint) (*AuthenticationResult, error) {
|
||||
if err := uc.users.SetUserAuthority(ctx, userID, authorityID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := uc.users.User(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issued, err := uc.issuer.IssueToken(user, authorityID, user.MustChangePassword, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uc.security.UseMultipoint() {
|
||||
oldToken, cacheErr := uc.security.ActiveToken(ctx, user.Username)
|
||||
if cacheErr != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr)
|
||||
}
|
||||
if cacheErr = uc.security.RotateActiveToken(ctx, user.Username, oldToken, issued.Value, issued.TTL); cacheErr != nil {
|
||||
if errors.Is(cacheErr, ErrTokenRevoke) {
|
||||
return nil, cacheErr
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr)
|
||||
}
|
||||
}
|
||||
return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: user.MustChangePassword}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user