feat(imLoginFailureLog): 用户登录密码错误次数锁定与解锁能力(管理端)
- 安全策略登录限频由 JSON 文本改为结构化表单(次数/锁定分钟/窗口分钟),编辑校验同步扩展 - 失败日志新增解锁入口:/imLoginFailureLog/unlock 经 internal 通道调用 im-server 释放锁定 - 失败日志模型补充 accountHash 字段,解锁按钮按 risk_action=account_locked 展示 - 租户业务库初始化模板补充 im_login_lockouts 等表;新增解锁按钮菜单权限 SQL
This commit is contained in:
@@ -947,6 +947,22 @@ CREATE TABLE IF NOT EXISTS `im_login_failure_logs` (
|
|||||||
KEY `idx_im_login_failure_request` (`request_id`)
|
KEY `idx_im_login_failure_request` (`request_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `im_login_lockouts` (
|
||||||
|
`id` bigint NOT NULL,
|
||||||
|
`user_id` bigint DEFAULT NULL,
|
||||||
|
`account_hash` char(64) NOT NULL,
|
||||||
|
`failed_attempts` int NOT NULL DEFAULT '0',
|
||||||
|
`window_started_at` datetime(3) DEFAULT NULL,
|
||||||
|
`locked_until` datetime(3) DEFAULT NULL,
|
||||||
|
`last_attempt_at` datetime(3) DEFAULT NULL,
|
||||||
|
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_im_login_lockouts_account` (`account_hash`),
|
||||||
|
KEY `idx_im_login_lockouts_user` (`user_id`),
|
||||||
|
KEY `idx_im_login_lockouts_locked` (`locked_until`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `im_mailbox_config` (
|
CREATE TABLE IF NOT EXISTS `im_mailbox_config` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
`enabled` tinyint NOT NULL DEFAULT '1',
|
`enabled` tinyint NOT NULL DEFAULT '1',
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- 登录失败锁定功能:登录失败日志页新增"解锁"按钮权限点
|
||||||
|
-- 依赖:im_adfahi_batch2_menu.sql(imLoginFailureLogIndex 菜单已存在)
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
SET @now := NOW();
|
||||||
|
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
|
||||||
|
SET @loginFailureId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imLoginFailureLogIndex' LIMIT 1);
|
||||||
|
|
||||||
|
INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`)
|
||||||
|
SELECT NULL,@loginFailureId,'解锁登录锁定','imLoginFailureLogUnlock','','3','','','/imLoginFailureLog/unlock','','','1','','0','0','','0','0','0','3',CONCAT('tr_',@imRootId,' tr_',@loginFailureId,' '),50,'','1',@now,@now
|
||||||
|
WHERE @loginFailureId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imLoginFailureLogUnlock');
|
||||||
@@ -99,13 +99,21 @@ type SecurityPolicyReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SecurityPolicyRes struct {
|
type SecurityPolicyRes struct {
|
||||||
TenantID int64 `json:"tenant_id"`
|
TenantID int64 `json:"tenant_id"`
|
||||||
TenantCode string `json:"tenant_code"`
|
TenantCode string `json:"tenant_code"`
|
||||||
PolicyVersion uint `json:"policy_version"`
|
PolicyVersion uint `json:"policy_version"`
|
||||||
GlobalFriendEnabled bool `json:"global_friend_enabled"`
|
GlobalFriendEnabled bool `json:"global_friend_enabled"`
|
||||||
FriendDisabledPolicy map[string]interface{} `json:"friend_disabled_policy"`
|
FriendDisabledPolicy map[string]interface{} `json:"friend_disabled_policy"`
|
||||||
MessageCurfewEnabled bool `json:"message_curfew_enabled"`
|
MessageCurfewEnabled bool `json:"message_curfew_enabled"`
|
||||||
CurfewRules []*SecurityCurfewRuleRes `json:"curfew_rules"`
|
LoginRateLimit *SecurityLoginRateLimitRes `json:"login_rate_limit"`
|
||||||
|
CurfewRules []*SecurityCurfewRuleRes `json:"curfew_rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecurityLoginRateLimitRes struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
PasswordMaxAttempts int `json:"password_max_attempts"`
|
||||||
|
LockSeconds int `json:"lock_seconds"`
|
||||||
|
WindowSeconds int `json:"window_seconds"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SecurityCurfewRuleRes struct {
|
type SecurityCurfewRuleRes struct {
|
||||||
|
|||||||
@@ -35,3 +35,8 @@ type BanUserReq struct {
|
|||||||
sysin.ImLoginFailureBanUserInp
|
sysin.ImLoginFailureBanUserInp
|
||||||
}
|
}
|
||||||
type BanUserRes struct{ *sysin.ImUserCommandModel }
|
type BanUserRes struct{ *sysin.ImUserCommandModel }
|
||||||
|
type UnlockReq struct {
|
||||||
|
g.Meta `path:"/imLoginFailureLog/unlock" method:"post" tags:"IM登录失败日志" summary:"解锁登录失败锁定"`
|
||||||
|
sysin.ImLoginFailureUnlockInp
|
||||||
|
}
|
||||||
|
type UnlockRes struct{}
|
||||||
|
|||||||
@@ -37,3 +37,6 @@ func (c *cImLoginFailureLog) BanUser(ctx context.Context, req *imloginfailurelog
|
|||||||
data, err := service.SysImLoginFailureLog().BanUser(ctx, &req.ImLoginFailureBanUserInp)
|
data, err := service.SysImLoginFailureLog().BanUser(ctx, &req.ImLoginFailureBanUserInp)
|
||||||
return &imloginfailurelog.BanUserRes{ImUserCommandModel: data}, err
|
return &imloginfailurelog.BanUserRes{ImUserCommandModel: data}, err
|
||||||
}
|
}
|
||||||
|
func (c *cImLoginFailureLog) Unlock(ctx context.Context, req *imloginfailurelog.UnlockReq) (*imloginfailurelog.UnlockRes, error) {
|
||||||
|
return &imloginfailurelog.UnlockRes{}, service.SysImLoginFailureLog().Unlock(ctx, &req.ImLoginFailureUnlockInp)
|
||||||
|
}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ func (c *cConfig) SecurityPolicy(ctx context.Context, req *api.SecurityPolicyReq
|
|||||||
}
|
}
|
||||||
columns := dao.ImSecurityPolicy.Columns()
|
columns := dao.ImSecurityPolicy.Columns()
|
||||||
record, err := imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
record, err := imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||||
Fields(columns.Id, columns.PolicyVersion, columns.GlobalFriendEnabled, "CAST("+columns.FriendDisabledPolicy+" AS CHAR) friend_disabled_policy", columns.MessageCurfewEnabled).
|
Fields(columns.Id, columns.PolicyVersion, columns.GlobalFriendEnabled, columns.MessageCurfewEnabled, "CAST("+columns.LoginRateLimit+" AS CHAR) login_rate_limit", "CAST("+columns.FriendDisabledPolicy+" AS CHAR) friend_disabled_policy").
|
||||||
Where(columns.TenantId, tenant.Id).Where(columns.PublishStatus, consts.ImSecurityPublishPublished).
|
Where(columns.TenantId, tenant.Id).Where(columns.PublishStatus, consts.ImSecurityPublishPublished).
|
||||||
WhereNull(columns.DeletedAt).OrderDesc(columns.PolicyVersion).One()
|
WhereNull(columns.DeletedAt).OrderDesc(columns.PolicyVersion).One()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -239,6 +239,22 @@ func (c *cConfig) SecurityPolicy(ctx context.Context, req *api.SecurityPolicyReq
|
|||||||
res.PolicyVersion = record[columns.PolicyVersion].Uint()
|
res.PolicyVersion = record[columns.PolicyVersion].Uint()
|
||||||
res.GlobalFriendEnabled = record[columns.GlobalFriendEnabled].Int() == 1
|
res.GlobalFriendEnabled = record[columns.GlobalFriendEnabled].Int() == 1
|
||||||
res.MessageCurfewEnabled = record[columns.MessageCurfewEnabled].Int() == 1
|
res.MessageCurfewEnabled = record[columns.MessageCurfewEnabled].Int() == 1
|
||||||
|
if raw := strings.TrimSpace(record["login_rate_limit"].String()); raw != "" && raw != "null" {
|
||||||
|
var limit struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
PasswordMaxAttempts int `json:"password_max_attempts"`
|
||||||
|
LockSeconds int `json:"lock_seconds"`
|
||||||
|
WindowSeconds int `json:"window_seconds"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal([]byte(raw), &limit) == nil && limit.Enabled {
|
||||||
|
res.LoginRateLimit = &api.SecurityLoginRateLimitRes{
|
||||||
|
Enabled: true,
|
||||||
|
PasswordMaxAttempts: limit.PasswordMaxAttempts,
|
||||||
|
LockSeconds: limit.LockSeconds,
|
||||||
|
WindowSeconds: limit.WindowSeconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if raw := strings.TrimSpace(record["friend_disabled_policy"].String()); raw != "" && raw != "null" {
|
if raw := strings.TrimSpace(record["friend_disabled_policy"].String()); raw != "" && raw != "null" {
|
||||||
_ = json.Unmarshal([]byte(raw), &res.FriendDisabledPolicy)
|
_ = json.Unmarshal([]byte(raw), &res.FriendDisabledPolicy)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"hotgo/internal/consts"
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/library/contexts"
|
"hotgo/internal/library/contexts"
|
||||||
|
"hotgo/internal/library/imadmin"
|
||||||
"hotgo/internal/library/imkafka"
|
"hotgo/internal/library/imkafka"
|
||||||
"hotgo/internal/model/input/form"
|
"hotgo/internal/model/input/form"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/gctx"
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,6 +130,26 @@ func (s *sSysImLoginFailureLog) AddBlacklist(ctx context.Context, in *sysin.ImLo
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *sSysImLoginFailureLog) Unlock(ctx context.Context, in *sysin.ImLoginFailureUnlockInp) error {
|
||||||
|
if err := in.Filter(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := "/internal/admin-query/tenants/" + url.PathEscape(tenant.Code) + "/login-lockouts/release"
|
||||||
|
var out struct {
|
||||||
|
AccountHash string `json:"account_hash"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]string{"account_hash": in.AccountHash}, &out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
g.Log().Infof(ctx, "登录锁定已解锁 tenantId:%d accountHash:%s adminId:%d status:%s", tenant.Id, in.AccountHash, contexts.GetUserId(ctx), out.Status)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sSysImLoginFailureLog) BanUser(ctx context.Context, in *sysin.ImLoginFailureBanUserInp) (*sysin.ImUserCommandModel, error) {
|
func (s *sSysImLoginFailureLog) BanUser(ctx context.Context, in *sysin.ImLoginFailureBanUserInp) (*sysin.ImUserCommandModel, error) {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ type ImLoginFailureLogModel struct {
|
|||||||
UserId string `json:"userId" dc:"用户ID"`
|
UserId string `json:"userId" dc:"用户ID"`
|
||||||
AccountType string `json:"accountType" dc:"账号类型"`
|
AccountType string `json:"accountType" dc:"账号类型"`
|
||||||
AccountMask string `json:"accountMask" dc:"脱敏账号"`
|
AccountMask string `json:"accountMask" dc:"脱敏账号"`
|
||||||
|
AccountHash string `json:"accountHash" dc:"账号哈希"`
|
||||||
FailureType string `json:"failureType" dc:"失败类型"`
|
FailureType string `json:"failureType" dc:"失败类型"`
|
||||||
Reason string `json:"reason" dc:"原因码"`
|
Reason string `json:"reason" dc:"原因码"`
|
||||||
Detail string `json:"detail" dc:"失败详情"`
|
Detail string `json:"detail" dc:"失败详情"`
|
||||||
@@ -130,6 +131,27 @@ func (in *ImLoginFailureAddBlacklistInp) Filter(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImLoginFailureUnlockInp struct {
|
||||||
|
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||||
|
AccountHash string `json:"accountHash" dc:"账号哈希"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *ImLoginFailureUnlockInp) Filter(ctx context.Context) error {
|
||||||
|
in.AccountHash = strings.ToLower(strings.TrimSpace(in.AccountHash))
|
||||||
|
if in.AccountHash == "" {
|
||||||
|
return gerror.New("账号哈希不能为空")
|
||||||
|
}
|
||||||
|
if len(in.AccountHash) != 64 {
|
||||||
|
return gerror.New("账号哈希格式不正确")
|
||||||
|
}
|
||||||
|
for _, c := range in.AccountHash {
|
||||||
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
||||||
|
return gerror.New("账号哈希格式不正确")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ImLoginFailureBanUserInp struct {
|
type ImLoginFailureBanUserInp struct {
|
||||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||||
UserId int64 `json:"userId" dc:"用户ID"`
|
UserId int64 `json:"userId" dc:"用户ID"`
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImSecurityPolicyListInp struct {
|
type ImSecurityPolicyListInp struct {
|
||||||
@@ -164,6 +165,13 @@ func (in *ImSecurityPolicySaveInp) Filter(ctx context.Context) error {
|
|||||||
if in.GlobalFriendEnabled != 0 && in.GlobalFriendEnabled != 1 {
|
if in.GlobalFriendEnabled != 0 && in.GlobalFriendEnabled != 1 {
|
||||||
return gerror.New("全局好友开关不合法")
|
return gerror.New("全局好友开关不合法")
|
||||||
}
|
}
|
||||||
|
if in.LoginRateLimit != nil {
|
||||||
|
normalized, err := normalizeLoginRateLimit(in.LoginRateLimit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
in.LoginRateLimit = normalized
|
||||||
|
}
|
||||||
if in.MessageCurfewEnabled != 0 && in.MessageCurfewEnabled != 1 {
|
if in.MessageCurfewEnabled != 0 && in.MessageCurfewEnabled != 1 {
|
||||||
return gerror.New("宵禁开关不合法")
|
return gerror.New("宵禁开关不合法")
|
||||||
}
|
}
|
||||||
@@ -210,6 +218,43 @@ func (in *ImSecurityPolicySaveInp) Filter(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeLoginRateLimit 校验并归一化登录失败锁定配置:
|
||||||
|
// {enabled, password_max_attempts, lock_seconds, window_seconds}。
|
||||||
|
// enabled=false 时输出仅含 enabled,行为与未配置一致。
|
||||||
|
func normalizeLoginRateLimit(value *gjson.Json) (*gjson.Json, error) {
|
||||||
|
policy := map[string]any{"enabled": false, "password_max_attempts": 5, "lock_seconds": 900, "window_seconds": 1800}
|
||||||
|
if value != nil && value.String() != "" && value.String() != "null" {
|
||||||
|
for key, item := range value.Map() {
|
||||||
|
policy[key] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if enabled, _ := policy["enabled"].(bool); !enabled {
|
||||||
|
return gjson.New(map[string]any{"enabled": false}), nil
|
||||||
|
}
|
||||||
|
maxAttempts := gconv.Int(policy["password_max_attempts"])
|
||||||
|
if maxAttempts == 0 {
|
||||||
|
maxAttempts = 5
|
||||||
|
}
|
||||||
|
if maxAttempts < 1 || maxAttempts > 20 {
|
||||||
|
return nil, gerror.New("登录失败最大次数必须在1至20之间")
|
||||||
|
}
|
||||||
|
lockSeconds := gconv.Int(policy["lock_seconds"])
|
||||||
|
if lockSeconds == 0 {
|
||||||
|
lockSeconds = 900
|
||||||
|
}
|
||||||
|
if lockSeconds < 60 || lockSeconds > 86400 {
|
||||||
|
return nil, gerror.New("账号锁定时长必须在1至1440分钟之间")
|
||||||
|
}
|
||||||
|
windowSeconds := gconv.Int(policy["window_seconds"])
|
||||||
|
if windowSeconds == 0 {
|
||||||
|
windowSeconds = 1800
|
||||||
|
}
|
||||||
|
if windowSeconds < 60 || windowSeconds > 14400 {
|
||||||
|
return nil, gerror.New("失败计数窗口必须在1至240分钟之间")
|
||||||
|
}
|
||||||
|
return gjson.New(map[string]any{"enabled": true, "password_max_attempts": maxAttempts, "lock_seconds": lockSeconds, "window_seconds": windowSeconds}), nil
|
||||||
|
}
|
||||||
|
|
||||||
type ImSecurityPolicyPublishInp struct {
|
type ImSecurityPolicyPublishInp struct {
|
||||||
Id int64 `json:"id" v:"required|min:1#策略ID不能为空|策略ID不合法"`
|
Id int64 `json:"id" v:"required|min:1#策略ID不能为空|策略ID不合法"`
|
||||||
Reason string `json:"reason" v:"required#发布原因不能为空"`
|
Reason string `json:"reason" v:"required#发布原因不能为空"`
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ type (
|
|||||||
Export(ctx context.Context, in *sysin.ImLoginFailureLogListInp) (err error)
|
Export(ctx context.Context, in *sysin.ImLoginFailureLogListInp) (err error)
|
||||||
AddBlacklist(ctx context.Context, in *sysin.ImLoginFailureAddBlacklistInp) (err error)
|
AddBlacklist(ctx context.Context, in *sysin.ImLoginFailureAddBlacklistInp) (err error)
|
||||||
BanUser(ctx context.Context, in *sysin.ImLoginFailureBanUserInp) (res *sysin.ImUserCommandModel, err error)
|
BanUser(ctx context.Context, in *sysin.ImLoginFailureBanUserInp) (res *sysin.ImUserCommandModel, err error)
|
||||||
|
Unlock(ctx context.Context, in *sysin.ImLoginFailureUnlockInp) (err error)
|
||||||
}
|
}
|
||||||
ISysImExternalConfig interface {
|
ISysImExternalConfig interface {
|
||||||
// List 获取IM外部服务配置列表
|
// List 获取IM外部服务配置列表
|
||||||
|
|||||||
@@ -15,3 +15,6 @@ export function AddBlacklist(params) {
|
|||||||
export function BanUser(params) {
|
export function BanUser(params) {
|
||||||
return http.request({ url: '/imLoginFailureLog/banUser', method: 'POST', params });
|
return http.request({ url: '/imLoginFailureLog/banUser', method: 'POST', params });
|
||||||
}
|
}
|
||||||
|
export function Unlock(params) {
|
||||||
|
return http.request({ url: '/imLoginFailureLog/unlock', method: 'POST', params });
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
import { BasicTable, TableAction } from '@/components/Table';
|
import { BasicTable, TableAction } from '@/components/Table';
|
||||||
import { usePermission } from '@/hooks/web/usePermission';
|
import { usePermission } from '@/hooks/web/usePermission';
|
||||||
import { adaTableScrollX } from '@/utils/hotgo';
|
import { adaTableScrollX } from '@/utils/hotgo';
|
||||||
import { AddBlacklist, BanUser, Export, List, View } from '@/api/imLoginFailureLog';
|
import { AddBlacklist, BanUser, Export, List, Unlock, View } from '@/api/imLoginFailureLog';
|
||||||
import { imQueryTenantOptions, loadImQueryTenantOptions } from '@/views/imAdminQueryShared';
|
import { imQueryTenantOptions, loadImQueryTenantOptions } from '@/views/imAdminQueryShared';
|
||||||
import { columns, loadOptions, schemas } from './model';
|
import { columns, loadOptions, schemas } from './model';
|
||||||
|
|
||||||
@@ -119,6 +119,13 @@
|
|||||||
ifShow: () => Number(record.userId) > 0,
|
ifShow: () => Number(record.userId) > 0,
|
||||||
onClick: () => openBan(record),
|
onClick: () => openBan(record),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '解锁',
|
||||||
|
type: 'primary',
|
||||||
|
auth: ['/imLoginFailureLog/unlock'],
|
||||||
|
ifShow: () => record.riskAction === 'account_locked' && !!record.accountHash,
|
||||||
|
onClick: () => unlockLockout(record),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -131,8 +138,7 @@
|
|||||||
});
|
});
|
||||||
const loadDataTable = async (res) => {
|
const loadDataTable = async (res) => {
|
||||||
const params = { ...searchFormRef.value?.formModel, ...res };
|
const params = { ...searchFormRef.value?.formModel, ...res };
|
||||||
if (!params.tenantId)
|
if (!params.tenantId) return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
||||||
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
|
||||||
return List(params);
|
return List(params);
|
||||||
};
|
};
|
||||||
function reloadTable() {
|
function reloadTable() {
|
||||||
@@ -177,6 +183,19 @@
|
|||||||
banReason.value = '';
|
banReason.value = '';
|
||||||
showBan.value = true;
|
showBan.value = true;
|
||||||
}
|
}
|
||||||
|
function unlockLockout(record) {
|
||||||
|
dialog.warning({
|
||||||
|
title: '解锁账号',
|
||||||
|
content: `确认解除 ${record.accountMask || '该账号'} 的登录锁定吗?解锁后可立即重新登录。`,
|
||||||
|
positiveText: '确认解锁',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
await Unlock({ tenantId: record.tenantId, accountHash: record.accountHash });
|
||||||
|
message.success('已解锁');
|
||||||
|
reloadTable();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
async function submitBan() {
|
async function submitBan() {
|
||||||
if (!banReason.value.trim()) {
|
if (!banReason.value.trim()) {
|
||||||
message.warning('请输入封禁原因');
|
message.warning('请输入封禁原因');
|
||||||
|
|||||||
@@ -219,12 +219,43 @@
|
|||||||
type="textarea"
|
type="textarea"
|
||||||
:autosize="{ minRows: 3, maxRows: 8 }"
|
:autosize="{ minRows: 3, maxRows: 8 }"
|
||||||
/></n-form-item>
|
/></n-form-item>
|
||||||
<n-form-item label="登录限频JSON"
|
<n-form-item label="登录失败锁定"
|
||||||
><n-input
|
><n-switch
|
||||||
v-model:value="policyForm.loginRateLimit"
|
v-model:value="policyForm.loginRateLimitEnabled"
|
||||||
type="textarea"
|
:checked-value="1"
|
||||||
:autosize="{ minRows: 3, maxRows: 8 }"
|
:unchecked-value="0"
|
||||||
/></n-form-item>
|
/></n-form-item>
|
||||||
|
<template v-if="policyForm.loginRateLimitEnabled">
|
||||||
|
<n-grid :cols="2" :x-gap="16">
|
||||||
|
<n-form-item-gi label="最大失败次数">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="policyForm.loginRateLimitMaxAttempts"
|
||||||
|
:min="1"
|
||||||
|
:max="20"
|
||||||
|
:precision="0"
|
||||||
|
/> </n-form-item-gi
|
||||||
|
><n-form-item-gi label="锁定时长(分钟)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="policyForm.loginRateLimitLockMinutes"
|
||||||
|
:min="1"
|
||||||
|
:max="1440"
|
||||||
|
:precision="0"
|
||||||
|
/> </n-form-item-gi
|
||||||
|
><n-form-item-gi label="计数窗口(分钟)">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="policyForm.loginRateLimitWindowMinutes"
|
||||||
|
:min="1"
|
||||||
|
:max="240"
|
||||||
|
:precision="0"
|
||||||
|
/>
|
||||||
|
</n-form-item-gi>
|
||||||
|
</n-grid>
|
||||||
|
<n-form-item label=" ">
|
||||||
|
<n-text depth="3">
|
||||||
|
同一账号在计数窗口内连续输错密码达到最大次数后锁定,锁定期间登录将被拒绝,可在“日志审计-登录失败日志”中解锁。
|
||||||
|
</n-text>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
<n-form-item label="实名策略JSON"
|
<n-form-item label="实名策略JSON"
|
||||||
><n-input
|
><n-input
|
||||||
v-model:value="policyForm.realnamePolicy"
|
v-model:value="policyForm.realnamePolicy"
|
||||||
@@ -412,7 +443,10 @@
|
|||||||
tenantId: tenantId(),
|
tenantId: tenantId(),
|
||||||
policyName: '安全策略',
|
policyName: '安全策略',
|
||||||
passwordPolicy: '{}',
|
passwordPolicy: '{}',
|
||||||
loginRateLimit: '{}',
|
loginRateLimitEnabled: 0,
|
||||||
|
loginRateLimitMaxAttempts: 5,
|
||||||
|
loginRateLimitLockMinutes: 15,
|
||||||
|
loginRateLimitWindowMinutes: 30,
|
||||||
sensitiveWordPolicy: '{}',
|
sensitiveWordPolicy: '{}',
|
||||||
moderationPolicy: defaultModerationPolicy(),
|
moderationPolicy: defaultModerationPolicy(),
|
||||||
fileReviewPolicy: '{}',
|
fileReviewPolicy: '{}',
|
||||||
@@ -518,6 +552,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await View({ id: row.id });
|
const data = await View({ id: row.id });
|
||||||
|
const loginRateLimit = parseJSON(data.loginRateLimit, {});
|
||||||
policyForm.value = {
|
policyForm.value = {
|
||||||
...data,
|
...data,
|
||||||
friendPolicy: {
|
friendPolicy: {
|
||||||
@@ -525,7 +560,13 @@
|
|||||||
...parseJSON(data.friendDisabledPolicy, {}),
|
...parseJSON(data.friendDisabledPolicy, {}),
|
||||||
},
|
},
|
||||||
passwordPolicy: data.passwordPolicy || '{}',
|
passwordPolicy: data.passwordPolicy || '{}',
|
||||||
loginRateLimit: data.loginRateLimit || '{}',
|
loginRateLimitEnabled: loginRateLimit.enabled ? 1 : 0,
|
||||||
|
loginRateLimitMaxAttempts: loginRateLimit.password_max_attempts || 5,
|
||||||
|
loginRateLimitLockMinutes: Math.max(1, Math.round((loginRateLimit.lock_seconds || 900) / 60)),
|
||||||
|
loginRateLimitWindowMinutes: Math.max(
|
||||||
|
1,
|
||||||
|
Math.round((loginRateLimit.window_seconds || 1800) / 60)
|
||||||
|
),
|
||||||
sensitiveWordPolicy: data.sensitiveWordPolicy || '{}',
|
sensitiveWordPolicy: data.sensitiveWordPolicy || '{}',
|
||||||
moderationPolicy: {
|
moderationPolicy: {
|
||||||
...defaultModerationPolicy(),
|
...defaultModerationPolicy(),
|
||||||
@@ -585,7 +626,14 @@
|
|||||||
await Save({
|
await Save({
|
||||||
...f,
|
...f,
|
||||||
passwordPolicy: jsonObject(f.passwordPolicy, '密码策略'),
|
passwordPolicy: jsonObject(f.passwordPolicy, '密码策略'),
|
||||||
loginRateLimit: jsonObject(f.loginRateLimit, '登录限频'),
|
loginRateLimit: f.loginRateLimitEnabled
|
||||||
|
? {
|
||||||
|
enabled: true,
|
||||||
|
password_max_attempts: f.loginRateLimitMaxAttempts,
|
||||||
|
lock_seconds: f.loginRateLimitLockMinutes * 60,
|
||||||
|
window_seconds: f.loginRateLimitWindowMinutes * 60,
|
||||||
|
}
|
||||||
|
: { enabled: false },
|
||||||
sensitiveWordPolicy: f.moderationPolicy,
|
sensitiveWordPolicy: f.moderationPolicy,
|
||||||
fileReviewPolicy: jsonObject(f.fileReviewPolicy, '文件审核策略'),
|
fileReviewPolicy: jsonObject(f.fileReviewPolicy, '文件审核策略'),
|
||||||
realnamePolicy: jsonObject(f.realnamePolicy, '实名策略'),
|
realnamePolicy: jsonObject(f.realnamePolicy, '实名策略'),
|
||||||
|
|||||||
Reference in New Issue
Block a user