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
+26
View File
@@ -12,6 +12,7 @@ package main
import (
"container/list"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
@@ -1150,9 +1151,29 @@ func (s *Session) login(msg *ClientComMessage) {
return
}
// Login failure lockout: reject already-locked accounts up front without
// comparing the password. A nil guard means the feature is not applicable.
lockGuard := newLoginLockoutGuard(s, msg.Login.Scheme, msg.Login.Secret)
if lockGuard != nil {
if retryAfter, locked := lockGuard.preCheck(); locked {
logs.Info.Printf("s.login: account locked, rejecting tenant:%d account_hash:%s sid:%s", s.tenantID, lockGuard.accountHash, s.sid)
s.queueOut(ErrAuthLocked(msg.Id, msg.Timestamp, loginLockedMessage(retryAfter),
map[string]any{"what": "auth", "retry_after_seconds": int(retryAfter.Seconds()) + 1}))
return
}
}
rec, challenge, err := handler.Authenticate(
auth.AuthContext{TenantID: s.tenantID, RemoteAddr: s.remoteAddr}, msg.Login.Secret)
if err != nil {
if lockGuard != nil && errors.Is(err, types.ErrFailed) {
if retryAfter, lockedNow := lockGuard.recordFailure(); lockedNow {
recordSessionLoginFailure(s, msg.Login.Scheme, msg.Login.Secret, rec, msg.Id, err, msg.Timestamp, loginAuditRiskActionLocked)
s.queueOut(ErrAuthLocked(msg.Id, msg.Timestamp, loginLockedMessage(retryAfter),
map[string]any{"what": "auth", "retry_after_seconds": int(retryAfter.Seconds()) + 1}))
return
}
}
recordSessionLoginFailure(s, msg.Login.Scheme, msg.Login.Secret, rec, msg.Id, err, msg.Timestamp)
resp := decodeStoreError(err, msg.Id, msg.Timestamp, nil)
if resp.Ctrl.Code >= 500 {
@@ -1170,6 +1191,11 @@ func (s *Session) login(msg *ClientComMessage) {
return
}
// Password accepted: clear the failure counter and any stale lock state.
if lockGuard != nil {
lockGuard.clear()
}
// If authenticator did not check user state, it returns state "undef". If so, check user state here.
if rec.State == types.StateUndefined {
rec.State, err = userGetState(s.tenantID, rec.Uid)