feat(imLoginFailureLog): 用户登录密码错误次数锁定与解锁能力(管理端)
- 安全策略登录限频由 JSON 文本改为结构化表单(次数/锁定分钟/窗口分钟),编辑校验同步扩展 - 失败日志新增解锁入口:/imLoginFailureLog/unlock 经 internal 通道调用 im-server 释放锁定 - 失败日志模型补充 accountHash 字段,解锁按钮按 risk_action=account_locked 展示 - 租户业务库初始化模板补充 im_login_lockouts 等表;新增解锁按钮菜单权限 SQL
This commit is contained in:
@@ -15,3 +15,6 @@ export function AddBlacklist(params) {
|
||||
export function BanUser(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 { usePermission } from '@/hooks/web/usePermission';
|
||||
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 { columns, loadOptions, schemas } from './model';
|
||||
|
||||
@@ -119,6 +119,13 @@
|
||||
ifShow: () => Number(record.userId) > 0,
|
||||
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 params = { ...searchFormRef.value?.formModel, ...res };
|
||||
if (!params.tenantId)
|
||||
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
||||
if (!params.tenantId) return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
||||
return List(params);
|
||||
};
|
||||
function reloadTable() {
|
||||
@@ -177,6 +183,19 @@
|
||||
banReason.value = '';
|
||||
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() {
|
||||
if (!banReason.value.trim()) {
|
||||
message.warning('请输入封禁原因');
|
||||
|
||||
@@ -219,12 +219,43 @@
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 8 }"
|
||||
/></n-form-item>
|
||||
<n-form-item label="登录限频JSON"
|
||||
><n-input
|
||||
v-model:value="policyForm.loginRateLimit"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 8 }"
|
||||
<n-form-item label="登录失败锁定"
|
||||
><n-switch
|
||||
v-model:value="policyForm.loginRateLimitEnabled"
|
||||
:checked-value="1"
|
||||
:unchecked-value="0"
|
||||
/></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-input
|
||||
v-model:value="policyForm.realnamePolicy"
|
||||
@@ -412,7 +443,10 @@
|
||||
tenantId: tenantId(),
|
||||
policyName: '安全策略',
|
||||
passwordPolicy: '{}',
|
||||
loginRateLimit: '{}',
|
||||
loginRateLimitEnabled: 0,
|
||||
loginRateLimitMaxAttempts: 5,
|
||||
loginRateLimitLockMinutes: 15,
|
||||
loginRateLimitWindowMinutes: 30,
|
||||
sensitiveWordPolicy: '{}',
|
||||
moderationPolicy: defaultModerationPolicy(),
|
||||
fileReviewPolicy: '{}',
|
||||
@@ -518,6 +552,7 @@
|
||||
return;
|
||||
}
|
||||
const data = await View({ id: row.id });
|
||||
const loginRateLimit = parseJSON(data.loginRateLimit, {});
|
||||
policyForm.value = {
|
||||
...data,
|
||||
friendPolicy: {
|
||||
@@ -525,7 +560,13 @@
|
||||
...parseJSON(data.friendDisabledPolicy, {}),
|
||||
},
|
||||
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 || '{}',
|
||||
moderationPolicy: {
|
||||
...defaultModerationPolicy(),
|
||||
@@ -585,7 +626,14 @@
|
||||
await Save({
|
||||
...f,
|
||||
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,
|
||||
fileReviewPolicy: jsonObject(f.fileReviewPolicy, '文件审核策略'),
|
||||
realnamePolicy: jsonObject(f.realnamePolicy, '实名策略'),
|
||||
|
||||
Reference in New Issue
Block a user