feat: 实现登录失败锁定功能

新增了登录失败次数统计和临时锁定机制,包括:
1. 定义登录锁定数据结构和数据库表结构
2. 实现租户级别的锁定状态持久化接口
3. 添加登录速率限制安全策略配置
4. 在登录流程中加入锁定前置检查和失败计数逻辑
5. 提供管理员手动解锁的内部API接口
6. 新增中文本地化的锁定提示文案
This commit is contained in:
Austin
2026-09-20 17:08:38 +08:00
parent 21063d4af0
commit 841c0c77cd
12 changed files with 563 additions and 3 deletions
+75
View File
@@ -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{}