feat: 实现登录失败锁定功能
新增了登录失败次数统计和临时锁定机制,包括: 1. 定义登录锁定数据结构和数据库表结构 2. 实现租户级别的锁定状态持久化接口 3. 添加登录速率限制安全策略配置 4. 在登录流程中加入锁定前置检查和失败计数逻辑 5. 提供管理员手动解锁的内部API接口 6. 新增中文本地化的锁定提示文案
This commit is contained in:
@@ -4438,6 +4438,80 @@ func actionTokenCacheID(uid types.Uid, tokenID, action, target string) string {
|
||||
return uid.String() + ":" + strings.TrimSpace(tokenID) + ":" + strings.TrimSpace(action) + ":" + strings.TrimSpace(target)
|
||||
}
|
||||
|
||||
// LoginLockoutPersistenceInterface stores per-account login failure counters
|
||||
// and temporary lock state in each tenant business database.
|
||||
type LoginLockoutPersistenceInterface interface {
|
||||
Get(tenantID types.TenantID, accountHash string) (*types.LoginLockout, error)
|
||||
RecordFailure(tenantID types.TenantID, accountHash string, userID *int64, windowCutoff, now time.Time) (*types.LoginLockout, error)
|
||||
Lock(tenantID types.TenantID, accountHash string, lockedUntil, now time.Time) error
|
||||
Reset(tenantID types.TenantID, accountHash string) error
|
||||
Release(tenantID types.TenantID, accountHash string, now time.Time) error
|
||||
}
|
||||
|
||||
type loginLockoutMapper struct{}
|
||||
|
||||
// LoginLockouts is the singleton for login lockout state.
|
||||
var LoginLockouts LoginLockoutPersistenceInterface
|
||||
|
||||
func tenantLoginLockoutAdapter(tenantID types.TenantID) (adapter.TenantLoginLockoutAdapter, error) {
|
||||
if !tenantID.IsValid() {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
if router, ok := adp.(adapter.TenantRoutedAdapter); ok {
|
||||
business, _, err := router.TenantAdapterFor(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lockAdp, ok := business.(adapter.TenantLoginLockoutAdapter); ok {
|
||||
return lockAdp, nil
|
||||
}
|
||||
}
|
||||
if lockAdp, ok := adp.(adapter.TenantLoginLockoutAdapter); ok {
|
||||
return lockAdp, nil
|
||||
}
|
||||
return nil, types.ErrUnsupported
|
||||
}
|
||||
|
||||
func (loginLockoutMapper) Get(tenantID types.TenantID, accountHash string) (*types.LoginLockout, error) {
|
||||
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lockAdp.TenantLoginLockoutGet(tenantID, accountHash)
|
||||
}
|
||||
|
||||
func (loginLockoutMapper) RecordFailure(tenantID types.TenantID, accountHash string, userID *int64, windowCutoff, now time.Time) (*types.LoginLockout, error) {
|
||||
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lockAdp.TenantLoginLockoutRecordFailure(tenantID, accountHash, userID, windowCutoff, now)
|
||||
}
|
||||
|
||||
func (loginLockoutMapper) Lock(tenantID types.TenantID, accountHash string, lockedUntil, now time.Time) error {
|
||||
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return lockAdp.TenantLoginLockoutLock(tenantID, accountHash, lockedUntil, now)
|
||||
}
|
||||
|
||||
func (loginLockoutMapper) Reset(tenantID types.TenantID, accountHash string) error {
|
||||
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return lockAdp.TenantLoginLockoutReset(tenantID, accountHash)
|
||||
}
|
||||
|
||||
func (loginLockoutMapper) Release(tenantID types.TenantID, accountHash string, now time.Time) error {
|
||||
lockAdp, err := tenantLoginLockoutAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return lockAdp.TenantLoginLockoutRelease(tenantID, accountHash, now)
|
||||
}
|
||||
|
||||
func tenantRegistrationInviteAdapter(tenantID types.TenantID) (adapter.TenantRegistrationInviteAdapter, error) {
|
||||
if !tenantID.IsValid() {
|
||||
return nil, types.ErrMalformed
|
||||
@@ -4606,6 +4680,7 @@ func init() {
|
||||
PCache = pcacheMapper{}
|
||||
Verifications = verificationMapper{}
|
||||
RegistrationInvites = registrationInviteMapper{}
|
||||
LoginLockouts = loginLockoutMapper{}
|
||||
Security = userSecurityMapper{}
|
||||
Relations = relationsMapper{}
|
||||
Favorites = favoritesMapper{}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// LoginLockout is the per-account login failure counter and lock state
|
||||
// stored in each tenant business database (table im_login_lockouts).
|
||||
type LoginLockout struct {
|
||||
TenantID TenantID `json:"tenant_id"`
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
AccountHash string `json:"account_hash"`
|
||||
FailedAttempts int `json:"failed_attempts"`
|
||||
WindowStartAt *time.Time `json:"window_started_at"`
|
||||
LockedUntil *time.Time `json:"locked_until"`
|
||||
LastAttemptAt *time.Time `json:"last_attempt_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -7,9 +7,37 @@ type SecurityPolicy struct {
|
||||
GlobalFriendEnabled bool `json:"global_friend_enabled"`
|
||||
FriendDisabledPolicy map[string]any `json:"friend_disabled_policy"`
|
||||
MessageCurfewEnabled bool `json:"message_curfew_enabled"`
|
||||
LoginRateLimit *LoginRateLimitPolicy `json:"login_rate_limit"`
|
||||
CurfewRules []*MessageCurfewRule `json:"curfew_rules"`
|
||||
}
|
||||
|
||||
// LoginRateLimitPolicy configures temporary account lockout after repeated
|
||||
// login password failures. Zero values keep the feature disabled.
|
||||
type LoginRateLimitPolicy struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PasswordMaxAttempts int `json:"password_max_attempts"`
|
||||
LockSeconds int `json:"lock_seconds"`
|
||||
WindowSeconds int `json:"window_seconds"`
|
||||
}
|
||||
|
||||
// Normalized returns defaults-completed limits; ok=false when disabled or invalid.
|
||||
func (p *LoginRateLimitPolicy) Normalized() (LoginRateLimitPolicy, bool) {
|
||||
if p == nil || !p.Enabled {
|
||||
return LoginRateLimitPolicy{}, false
|
||||
}
|
||||
normalized := *p
|
||||
if normalized.PasswordMaxAttempts <= 0 {
|
||||
normalized.PasswordMaxAttempts = 5
|
||||
}
|
||||
if normalized.LockSeconds <= 0 {
|
||||
normalized.LockSeconds = 900
|
||||
}
|
||||
if normalized.WindowSeconds <= 0 {
|
||||
normalized.WindowSeconds = 1800
|
||||
}
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
type MessageCurfewRule struct {
|
||||
RuleCode string `json:"rule_code"`
|
||||
RuleName string `json:"rule_name"`
|
||||
|
||||
Reference in New Issue
Block a user