From e800cd4b051c93b0dc79c7a0436e46e36bd10099 Mon Sep 17 00:00:00 2001 From: qian Date: Tue, 1 Sep 2026 21:34:45 +0800 Subject: [PATCH] =?UTF-8?q?fix(setting):=E7=A7=BB=E9=99=A4=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E9=85=8D=E7=BD=AE=E4=B8=AD=E7=9A=84=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/logic/admin/auth.go | 9 ++-- internal/logic/common/security.go | 14 +++++++ internal/logic/common/settings_registry.go | 13 ++---- internal/logic/tenant/auth.go | 9 ++-- .../src/views/system/settings/SecurityTab.vue | 41 ------------------- 5 files changed, 27 insertions(+), 59 deletions(-) diff --git a/internal/logic/admin/auth.go b/internal/logic/admin/auth.go index 9d2b3bc..aea3726 100644 --- a/internal/logic/admin/auth.go +++ b/internal/logic/admin/auth.go @@ -84,12 +84,13 @@ func (s *sAdmin) Login(ctx context.Context, req *v1.AdminLoginReq) (*v1.AdminLog if !crypto.VerifyPassword(req.Password, user.PasswordHash) { _ = common.RecordLoginHistory(ctx, "admin", user.Id, 0, "password", ipAddress, ua, deviceFP, false, "密码错误") - // 原子递增失败次数 + // 原子递增失败次数;锁定阈值与时长取自安全配置(sys_options) updateData := do.SysAdminUsers{FailedAttempts: gdb.Raw("failed_attempts + 1")} failedAttempts := user.FailedAttempts + 1 + maxAttempts, lockoutMinutes := common.LoginLockoutPolicy(ctx) - if failedAttempts >= 5 { - lockedUntil := time.Now().Add(30 * time.Minute) + if failedAttempts >= maxAttempts { + lockedUntil := time.Now().Add(time.Duration(lockoutMinutes) * time.Minute) updateData.LockedUntil = gtime.NewFromTime(lockedUntil) _, err := dao.SysAdminUsers.Ctx(ctx). Where("id", user.Id). @@ -98,7 +99,7 @@ func (s *sAdmin) Login(ctx context.Context, req *v1.AdminLoginReq) (*v1.AdminLog g.Log().Errorf(ctx, "更新管理员账号锁定状态失败: %v", err) } return nil, common.NewBusinessError(consts.CodeAccountLocked, - "连续 5 次密码错误,账号已锁定 30 分钟") + fmt.Sprintf("连续 %d 次密码错误,账号已锁定 %d 分钟", maxAttempts, lockoutMinutes)) } _, err := dao.SysAdminUsers.Ctx(ctx). diff --git a/internal/logic/common/security.go b/internal/logic/common/security.go index db60e1e..bf8f2e3 100644 --- a/internal/logic/common/security.go +++ b/internal/logic/common/security.go @@ -418,6 +418,20 @@ func RegenerateBackupCodes(ctx context.Context, userType string, userID int64, c return plainCodes, nil } +// LoginLockoutPolicy 返回登录锁定策略(安全配置 sys_options:login_max_attempts / login_lockout_minutes)。 +// 未配置或非法值(<=0)时回退默认 5 次 / 30 分钟,管理后台与租户控制台共用同一策略。 +func LoginLockoutPolicy(ctx context.Context) (maxAttempts, lockoutMinutes int) { + maxAttempts = Config().GetInt(ctx, "login_max_attempts") + if maxAttempts <= 0 { + maxAttempts = 5 + } + lockoutMinutes = Config().GetInt(ctx, "login_lockout_minutes") + if lockoutMinutes <= 0 { + lockoutMinutes = 30 + } + return maxAttempts, lockoutMinutes +} + // Is2FAEnabled checks if 2FA is enabled for a user. func Is2FAEnabled(ctx context.Context, userType string, userID int64) (bool, error) { if userType == "admin" { diff --git a/internal/logic/common/settings_registry.go b/internal/logic/common/settings_registry.go index a3542ee..22efeb2 100644 --- a/internal/logic/common/settings_registry.go +++ b/internal/logic/common/settings_registry.go @@ -113,16 +113,11 @@ var Registry = []SettingDef{ Label: "启用 TLS", Description: "587/465 端口建议开启"}, // ── Security ── - {Key: "max_sessions_per_user", Type: SettingTypeInt, Default: "10", Category: "security", - Label: "租户用户最大会话数", Validation: "min:1,max:100"}, - {Key: "admin_max_sessions", Type: SettingTypeInt, Default: "5", Category: "security", - Label: "管理员最大会话数", Validation: "min:1,max:50"}, + // 会话数上限不入注册表:并发会话限制由配置文件 jwt.adminMaxSessions / jwt.tenantMaxSessions 管理 {Key: "login_max_attempts", Type: SettingTypeInt, Default: "5", Category: "security", - Label: "登录最大尝试次数", Validation: "min:1,max:30"}, + Label: "登录最大尝试次数", Description: "连续密码错误达到该次数后锁定账号(管理后台 + 租户控制台)", Validation: "min:1,max:30"}, {Key: "login_lockout_minutes", Type: SettingTypeInt, Default: "30", Category: "security", - Label: "登录锁定时长(分钟)", Validation: "min:1,max:1440"}, - {Key: "password_min_length", Type: SettingTypeInt, Default: "8", Category: "security", - Label: "密码最小长度", Validation: "min:6,max:32"}, + Label: "登录锁定时长(分钟)", Description: "触发锁定后的持续时长(管理后台 + 租户控制台)", Validation: "min:1,max:1440"}, {Key: "turnstile_enabled", Type: SettingTypeBool, Default: "false", Category: "security", Label: "启用 Turnstile 人机验证", IsPublic: true}, {Key: "turnstile_site_key", Type: SettingTypeString, Default: "", Category: "security", @@ -131,8 +126,6 @@ var Registry = []SettingDef{ Label: "Turnstile Secret Key", Sensitive: true}, {Key: "captcha_expire_seconds", Type: SettingTypeInt, Default: "300", Category: "security", Label: "验证码有效期(秒)", Validation: "min:60,max:600"}, - {Key: "new_device_notification", Type: SettingTypeBool, Default: "true", Category: "security", - Label: "新设备登录通知"}, {Key: "register_forbidden_words", Type: SettingTypeString, Default: "admin,system,root,api,test,administrator,管理员,系统", Category: "security", Label: "注册禁用词", Description: "组织名称、组织代码、用户名中包含这些词时禁止注册(不区分大小写),多个禁用词用英文逗号分隔"}, {Key: "sandbox_enabled", Type: SettingTypeBool, Default: "true", Category: "performance", diff --git a/internal/logic/tenant/auth.go b/internal/logic/tenant/auth.go index 0dd40e4..789ee7f 100644 --- a/internal/logic/tenant/auth.go +++ b/internal/logic/tenant/auth.go @@ -412,12 +412,13 @@ func (s *sTenant) Login(ctx context.Context, req *v1.TenantLoginReq) (*v1.Tenant if !crypto.VerifyPassword(req.Password, user.PasswordHash) { _ = common.RecordLoginHistory(ctx, "tenant", user.Id, tenant.Id, "password", ipAddress, ua, deviceFP, false, "密码错误") - // 原子递增失败次数 + // 原子递增失败次数;锁定阈值与时长取自安全配置(sys_options) updateData := do.TntUsers{FailedAttempts: gdb.Raw("failed_attempts + 1")} failedAttempts := user.FailedAttempts + 1 + maxAttempts, lockoutMinutes := common.LoginLockoutPolicy(ctx) - if failedAttempts >= 5 { - lockedUntil := time.Now().Add(30 * time.Minute) + if failedAttempts >= maxAttempts { + lockedUntil := time.Now().Add(time.Duration(lockoutMinutes) * time.Minute) updateData.LockedUntil = gtime.NewFromTime(lockedUntil) _, err := dao.TntUsers.Ctx(ctx). Where("id", user.Id). @@ -426,7 +427,7 @@ func (s *sTenant) Login(ctx context.Context, req *v1.TenantLoginReq) (*v1.Tenant g.Log().Errorf(ctx, "更新账号锁定状态失败: %v", err) } return nil, common.NewBusinessError(consts.CodeAccountLocked, - "连续 5 次密码错误,账号已锁定 30 分钟") + fmt.Sprintf("连续 %d 次密码错误,账号已锁定 %d 分钟", maxAttempts, lockoutMinutes)) } _, err := dao.TntUsers.Ctx(ctx). diff --git a/web/admin/src/views/system/settings/SecurityTab.vue b/web/admin/src/views/system/settings/SecurityTab.vue index 1e87770..4ff78ec 100644 --- a/web/admin/src/views/system/settings/SecurityTab.vue +++ b/web/admin/src/views/system/settings/SecurityTab.vue @@ -5,27 +5,6 @@ const values = useFormValues()