feat(im security): add login failure lockout configuration feature
1. add APIs for login lockout config get and save 2. add backend service and controller for login lockout config 3. refactor frontend to separate login lockout config from main policy 4. update default login lockout policy values 5. add permission sql and docking documentation
This commit is contained in:
@@ -10,6 +10,10 @@ export const Publish = (params) =>
|
||||
http.request({ url: '/imSecurityPolicy/publish', method: 'POST', params });
|
||||
export const RestoreDefault = (params) =>
|
||||
http.request({ url: '/imSecurityPolicy/restoreDefault', method: 'POST', params });
|
||||
export const GetLoginLockoutConfig = (params) =>
|
||||
http.request({ url: '/imSecurityPolicy/loginLockoutConfig', method: 'GET', params });
|
||||
export const SaveLoginLockoutConfig = (params) =>
|
||||
http.request({ url: '/imSecurityPolicy/loginLockoutConfig', method: 'POST', params });
|
||||
export const IPRuleList = (params) =>
|
||||
http.request({ url: '/imSecurityPolicy/ipRuleList', method: 'GET', params });
|
||||
export const IPRuleEdit = (params) =>
|
||||
|
||||
@@ -64,11 +64,11 @@
|
||||
ref="policyFormRef"
|
||||
:model="policyForm"
|
||||
:rules="policyRules"
|
||||
:disabled="policyReadOnly"
|
||||
:disabled="policyReadOnly && policyActiveTab !== 'lockout'"
|
||||
label-placement="left"
|
||||
label-width="132"
|
||||
>
|
||||
<n-tabs type="segment" animated>
|
||||
<n-tabs v-model:value="policyActiveTab" type="segment" animated>
|
||||
<n-tab-pane name="friend" tab="好友控制">
|
||||
<n-form-item label="策略名称" path="policyName"
|
||||
><n-input v-model:value="policyForm.policyName"
|
||||
@@ -212,6 +212,51 @@
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="lockout" tab="登录失败策略">
|
||||
<n-form-item label="登录失败锁定"
|
||||
><n-switch v-model:value="lockoutForm.enabled"
|
||||
/></n-form-item>
|
||||
<template v-if="lockoutForm.enabled">
|
||||
<n-grid :cols="2" :x-gap="16">
|
||||
<n-form-item-gi label="最大失败次数">
|
||||
<n-input-number
|
||||
v-model:value="lockoutForm.passwordMaxAttempts"
|
||||
:min="1"
|
||||
:max="20"
|
||||
:precision="0"
|
||||
/> </n-form-item-gi
|
||||
><n-form-item-gi label="锁定时长(分钟)">
|
||||
<n-input-number
|
||||
v-model:value="lockoutForm.lockMinutes"
|
||||
:min="1"
|
||||
:max="1440"
|
||||
:precision="0"
|
||||
/> </n-form-item-gi
|
||||
><n-form-item-gi label="计数窗口(分钟)">
|
||||
<n-input-number
|
||||
v-model:value="lockoutForm.windowMinutes"
|
||||
:min="1"
|
||||
:max="240"
|
||||
:precision="0"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
<n-form-item label=" ">
|
||||
<n-text depth="3">
|
||||
同一账号在计数窗口内连续输错密码达到最大次数后锁定,锁定期间登录将被拒绝,可在“日志审计-登录失败日志”中解锁。保存后立即生效,无需发布策略。
|
||||
</n-text>
|
||||
</n-form-item>
|
||||
<n-space justify="end">
|
||||
<n-button
|
||||
v-if="hasPermission(['/imSecurityPolicy/loginLockoutConfig'])"
|
||||
type="primary"
|
||||
:loading="lockoutSaving"
|
||||
@click="saveLockoutConfig"
|
||||
>保存</n-button
|
||||
>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="advanced" tab="其他策略">
|
||||
<n-form-item label="密码策略JSON"
|
||||
><n-input
|
||||
@@ -219,43 +264,6 @@
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 8 }"
|
||||
/></n-form-item>
|
||||
<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"
|
||||
@@ -332,6 +340,7 @@
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
|
||||
import {
|
||||
GetLoginLockoutConfig,
|
||||
IPRuleDelete,
|
||||
IPRuleEdit,
|
||||
IPRuleList,
|
||||
@@ -339,6 +348,7 @@
|
||||
Publish,
|
||||
RestoreDefault,
|
||||
Save,
|
||||
SaveLoginLockoutConfig,
|
||||
View,
|
||||
} from '@/api/imSecurityPolicy';
|
||||
import { columns, ipColumns, loadOptions, schemas } from './model';
|
||||
@@ -359,6 +369,7 @@
|
||||
policyFormRef = ref(),
|
||||
ipFormRef = ref();
|
||||
const activeTab = ref('policy'),
|
||||
policyActiveTab = ref('friend'),
|
||||
policyVisible = ref(false),
|
||||
ipVisible = ref(false),
|
||||
submitting = ref(false);
|
||||
@@ -443,10 +454,6 @@
|
||||
tenantId: tenantId(),
|
||||
policyName: '安全策略',
|
||||
passwordPolicy: '{}',
|
||||
loginRateLimitEnabled: 0,
|
||||
loginRateLimitMaxAttempts: 5,
|
||||
loginRateLimitLockMinutes: 15,
|
||||
loginRateLimitWindowMinutes: 30,
|
||||
sensitiveWordPolicy: '{}',
|
||||
moderationPolicy: defaultModerationPolicy(),
|
||||
fileReviewPolicy: '{}',
|
||||
@@ -474,6 +481,46 @@
|
||||
() => policyForm.value.id > 0 && policyForm.value.publishStatus !== 'draft'
|
||||
);
|
||||
const policyRules = { policyName: { required: true, message: '请输入策略名称' } };
|
||||
const emptyLockout = () => ({
|
||||
enabled: false,
|
||||
passwordMaxAttempts: 5,
|
||||
lockMinutes: 1440,
|
||||
windowMinutes: 60,
|
||||
policyId: 0,
|
||||
});
|
||||
const lockoutForm = ref<any>(emptyLockout());
|
||||
const lockoutSaving = ref(false);
|
||||
async function loadLockoutConfig(tenant = 0) {
|
||||
try {
|
||||
const data = await GetLoginLockoutConfig({ tenantId: tenant });
|
||||
lockoutForm.value = {
|
||||
enabled: !!data.enabled,
|
||||
passwordMaxAttempts: data.passwordMaxAttempts || 5,
|
||||
lockMinutes: data.lockMinutes || 1440,
|
||||
windowMinutes: data.windowMinutes || 60,
|
||||
policyId: data.policyId || 0,
|
||||
};
|
||||
} catch {
|
||||
lockoutForm.value = emptyLockout();
|
||||
}
|
||||
}
|
||||
async function saveLockoutConfig() {
|
||||
lockoutSaving.value = true;
|
||||
try {
|
||||
await SaveLoginLockoutConfig({
|
||||
tenantId: policyForm.value.tenantId || tenantId(),
|
||||
enabled: lockoutForm.value.enabled,
|
||||
passwordMaxAttempts: lockoutForm.value.passwordMaxAttempts,
|
||||
lockMinutes: lockoutForm.value.lockMinutes,
|
||||
windowMinutes: lockoutForm.value.windowMinutes,
|
||||
});
|
||||
message.success('已保存,配置即时生效');
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '保存失败');
|
||||
} finally {
|
||||
lockoutSaving.value = false;
|
||||
}
|
||||
}
|
||||
const emptyIP = () => ({
|
||||
id: 0,
|
||||
tenantId: tenantId(),
|
||||
@@ -546,13 +593,14 @@
|
||||
const policyScrollX = computed(() => adaTableScrollX(columns, actionColumn.width)),
|
||||
ipScrollX = computed(() => adaTableScrollX(ipColumns, ipActionColumn.width));
|
||||
async function openPolicy(row?: any) {
|
||||
policyActiveTab.value = 'friend';
|
||||
if (!row) {
|
||||
policyForm.value = emptyPolicy();
|
||||
policyVisible.value = true;
|
||||
loadLockoutConfig(tenantId());
|
||||
return;
|
||||
}
|
||||
const data = await View({ id: row.id });
|
||||
const loginRateLimit = parseJSON(data.loginRateLimit, {});
|
||||
policyForm.value = {
|
||||
...data,
|
||||
friendPolicy: {
|
||||
@@ -560,13 +608,6 @@
|
||||
...parseJSON(data.friendDisabledPolicy, {}),
|
||||
},
|
||||
passwordPolicy: data.passwordPolicy || '{}',
|
||||
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(),
|
||||
@@ -592,7 +633,10 @@
|
||||
whitelistUserText: parseJSON(v.whitelistUserIds, []).join(','),
|
||||
})),
|
||||
};
|
||||
// 登录失败锁定配置已迁移至独立tab管理,避免随草稿保存回传覆盖
|
||||
delete policyForm.value.loginRateLimit;
|
||||
policyVisible.value = true;
|
||||
loadLockoutConfig(policyForm.value.tenantId);
|
||||
}
|
||||
function addCurfew() {
|
||||
policyForm.value.curfewRules.push(newCurfew(policyForm.value.curfewRules.length + 1));
|
||||
@@ -626,14 +670,6 @@
|
||||
await Save({
|
||||
...f,
|
||||
passwordPolicy: jsonObject(f.passwordPolicy, '密码策略'),
|
||||
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