feat: 增加闲置超时退出控制
This commit is contained in:
parent
7cc86ce424
commit
9e879f5c2f
@ -131,6 +131,11 @@ public class ConfigConst
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SysI18NSwitch = "sys_i18n_switch";
|
public const string SysI18NSwitch = "sys_i18n_switch";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 闲置超时时间
|
||||||
|
/// </summary>
|
||||||
|
public const string SysIdleTimeout = "sys_idle_timeout";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 支付宝授权页面地址
|
/// 支付宝授权页面地址
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -39,6 +39,7 @@ public class SysConfigSeedData : ISqlSugarEntitySeedData<SysConfig>
|
|||||||
new SysConfig{ Id=1300000000261, Name="密码历史记录验证", Code=ConfigConst.SysPasswordRecord, Value="False", SysFlag=YesNoEnum.Y, Remark="是否验证历史密码禁止再次使用", OrderNo=210, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-17 00:00:00") },
|
new SysConfig{ Id=1300000000261, Name="密码历史记录验证", Code=ConfigConst.SysPasswordRecord, Value="False", SysFlag=YesNoEnum.Y, Remark="是否验证历史密码禁止再次使用", OrderNo=210, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-17 00:00:00") },
|
||||||
new SysConfig{ Id=1300000000271, Name="显示系统更新日志", Code=ConfigConst.SysUpgrade, Value="True", SysFlag=YesNoEnum.Y, Remark="是否显示系统更新日志", OrderNo=220, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
|
new SysConfig{ Id=1300000000271, Name="显示系统更新日志", Code=ConfigConst.SysUpgrade, Value="True", SysFlag=YesNoEnum.Y, Remark="是否显示系统更新日志", OrderNo=220, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
|
||||||
new SysConfig{ Id=1300000000281, Name="开启多语言切换", Code=ConfigConst.SysI18NSwitch, Value="True", SysFlag=YesNoEnum.Y, Remark="是否显示多语言切换按钮", OrderNo=230, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
|
new SysConfig{ Id=1300000000281, Name="开启多语言切换", Code=ConfigConst.SysI18NSwitch, Value="True", SysFlag=YesNoEnum.Y, Remark="是否显示多语言切换按钮", OrderNo=230, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
|
||||||
|
new SysConfig{ Id=1300000000282, Name="闲置超时时间", Code=ConfigConst.SysIdleTimeout, Value="0", SysFlag=YesNoEnum.Y, Remark="闲置超时时间(分钟),超时强制退出,0 表示不限制", OrderNo=240, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
|
||||||
|
|
||||||
new SysConfig{ Id=1300000000999, Name="系统版本号", Code=ConfigConst.SysVersion, Value="0", SysFlag=YesNoEnum.Y, Remark= "系统版本号,用于自动升级,请勿手动填写", OrderNo=1000, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2025-04-10 00:00:00") },
|
new SysConfig{ Id=1300000000999, Name="系统版本号", Code=ConfigConst.SysVersion, Value="0", SysFlag=YesNoEnum.Y, Remark= "系统版本号,用于自动升级,请勿手动填写", OrderNo=1000, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2025-04-10 00:00:00") },
|
||||||
];
|
];
|
||||||
|
|||||||
@ -588,6 +588,7 @@ public class SysTenantService : IDynamicApiController, ITransient
|
|||||||
var forceChangePassword = await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysForceChangePassword); // 强制修改密码
|
var forceChangePassword = await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysForceChangePassword); // 强制修改密码
|
||||||
var passwordExpirationTime = await _sysConfigService.GetConfigValueByCode<int>(ConfigConst.SysPasswordExpirationTime); // 密码有效期
|
var passwordExpirationTime = await _sysConfigService.GetConfigValueByCode<int>(ConfigConst.SysPasswordExpirationTime); // 密码有效期
|
||||||
var i18NSwitch = await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysI18NSwitch); // 开启多语言切换
|
var i18NSwitch = await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysI18NSwitch); // 开启多语言切换
|
||||||
|
var idleTimeout = await _sysConfigService.GetConfigValueByCode<int>(ConfigConst.SysIdleTimeout); // 闲置超时时间
|
||||||
var publicKey = App.GetConfig<string>("Cryptogram:PublicKey", true); // 获取密码加解密公钥配置
|
var publicKey = App.GetConfig<string>("Cryptogram:PublicKey", true); // 获取密码加解密公钥配置
|
||||||
|
|
||||||
return new
|
return new
|
||||||
@ -612,6 +613,7 @@ public class SysTenantService : IDynamicApiController, ITransient
|
|||||||
PublicKey = publicKey,
|
PublicKey = publicKey,
|
||||||
CarouselFiles = carouselFiles,
|
CarouselFiles = carouselFiles,
|
||||||
I18NSwitch = i18NSwitch,
|
I18NSwitch = i18NSwitch,
|
||||||
|
IdleTimeout = idleTimeout,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { Local, Session } from '/@/utils/storage';
|
|||||||
import mittBus from '/@/utils/mitt';
|
import mittBus from '/@/utils/mitt';
|
||||||
import setIntroduction from '/@/utils/setIconfont';
|
import setIntroduction from '/@/utils/setIconfont';
|
||||||
// import Watermark from '/@/utils/watermark';
|
// import Watermark from '/@/utils/watermark';
|
||||||
|
import { initIdleTimeout } from './utils/idleTimeout';
|
||||||
|
|
||||||
// 引入组件
|
// 引入组件
|
||||||
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
|
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
|
||||||
@ -102,6 +103,11 @@ document.body.ondrop = function (event) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 初始化全局空闲超时
|
||||||
|
initIdleTimeout({
|
||||||
|
timeout: themeConfig.value.idleTimeout,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@ -353,5 +353,7 @@ export default {
|
|||||||
confirmCopyRole: '确定复制角色:【{roleName}】?',
|
confirmCopyRole: '确定复制角色:【{roleName}】?',
|
||||||
authAccount: '授权账号',
|
authAccount: '授权账号',
|
||||||
successCopy: '复制成功',
|
successCopy: '复制成功',
|
||||||
|
sysMessage: '系统消息',
|
||||||
|
idleTimeoutMessage: '长时间未操作,已退出系统',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -154,6 +154,8 @@ export const useThemeConfig = defineStore('themeConfig', {
|
|||||||
icpUrl: 'https://beian.miit.gov.cn',
|
icpUrl: 'https://beian.miit.gov.cn',
|
||||||
// 是否开启多语言切换
|
// 是否开启多语言切换
|
||||||
i18NSwitch: true,
|
i18NSwitch: true,
|
||||||
|
// 闲置超时时间
|
||||||
|
idleTimeout: 0,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
|||||||
1
Web/src/types/pinia.d.ts
vendored
1
Web/src/types/pinia.d.ts
vendored
@ -105,5 +105,6 @@ declare interface ThemeConfigState {
|
|||||||
forceChangePassword?: boolean; // 是否开启强制修改密码
|
forceChangePassword?: boolean; // 是否开启强制修改密码
|
||||||
passwordExpirationTime?: number; // 是否验证密码有效期
|
passwordExpirationTime?: number; // 是否验证密码有效期
|
||||||
i18NSwitch: boolean; // 是否开启多语言切换
|
i18NSwitch: boolean; // 是否开启多语言切换
|
||||||
|
idleTimeout: number; // 闲置超时时间
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
165
Web/src/utils/idleTimeout.ts
Normal file
165
Web/src/utils/idleTimeout.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
import { Local, Session } from '/@/utils/storage';
|
||||||
|
import { signalR } from '/@/views/system/onlineUser/signalR';
|
||||||
|
import { ElMessageBox } from 'element-plus';
|
||||||
|
import { accessTokenKey, refreshAccessTokenKey } from '/@/utils/axios-utils';
|
||||||
|
import { i18n } from '/@/i18n';
|
||||||
|
|
||||||
|
type IdleTimeoutConfig = {
|
||||||
|
/** 空闲超时时间(秒),默认30分钟 */
|
||||||
|
timeout?: number;
|
||||||
|
/** 用于设置最后活动时间的监听事件列表 */
|
||||||
|
events?: string[];
|
||||||
|
/** 登出回调函数,在超时发生时执行 */
|
||||||
|
onTimeout?: () => void;
|
||||||
|
/** 监听事件防抖间隔(毫秒),默认 200,设为0表示不启用防抖 */
|
||||||
|
debounceInterval?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class IdleTimeoutManager {
|
||||||
|
private timerId: number | null = null;
|
||||||
|
private readonly config: Required<IdleTimeoutConfig>;
|
||||||
|
private readonly debouncedReset: () => void;
|
||||||
|
/** 检查闲置超时时间间隔 */
|
||||||
|
private readonly checkTimeoutInterval = 2 * 1000;
|
||||||
|
|
||||||
|
constructor(config: IdleTimeoutConfig = {}) {
|
||||||
|
this.config = {
|
||||||
|
timeout: 30 * 60,
|
||||||
|
events: ['mousewheel', 'keydown', 'click'],
|
||||||
|
onTimeout: this.timeOutExec.bind(this),
|
||||||
|
debounceInterval: 200,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 防抖处理
|
||||||
|
this.debouncedReset = this.config.debounceInterval > 0 ? debounce(this.setLastActivityTime.bind(this), this.config.debounceInterval) : this.setLastActivityTime.bind(this);
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
// 初始化事件监听
|
||||||
|
this.config.events.forEach((event) => {
|
||||||
|
window.addEventListener(event, this.debouncedReset);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面可见性监听
|
||||||
|
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
||||||
|
|
||||||
|
// 设置最后活动时间
|
||||||
|
this.setLastActivityTime();
|
||||||
|
|
||||||
|
// 更新空闲超时时间(会按需启动检查定时器)
|
||||||
|
this.updateIdleTimeout(this.config.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
this.setLastActivityTime();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 设置最后活动时间 */
|
||||||
|
public setLastActivityTime() {
|
||||||
|
Local.set('lastActivityTime', new Date().getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新空闲超时时间
|
||||||
|
* @param timeout - 新的超时时间(毫秒)
|
||||||
|
*/
|
||||||
|
public updateIdleTimeout(timeout: number) {
|
||||||
|
this.config.timeout = timeout;
|
||||||
|
// 如果闲置超时时间大于0且检测定时器没启动
|
||||||
|
if (this.config.timeout > 0 && this.timerId == null) {
|
||||||
|
this.timerId = window.setInterval(this.checkTimeout.bind(this), this.checkTimeoutInterval);
|
||||||
|
} else if (this.config.timeout == 0 && this.timerId != null) {
|
||||||
|
window.clearInterval(this.timerId);
|
||||||
|
this.timerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 检查是否超时 */
|
||||||
|
public checkTimeout() {
|
||||||
|
const currentTime = new Date().getTime(); // 当前时间
|
||||||
|
const lastActivityTime = Number(Local.get('lastActivityTime'));
|
||||||
|
if (lastActivityTime == 0) return;
|
||||||
|
const accessToken = Local.get(accessTokenKey);
|
||||||
|
if (!accessToken || accessToken == 'invalid_token') return;
|
||||||
|
const timeout = this.config.timeout * 1000;
|
||||||
|
if (currentTime - lastActivityTime > timeout) {
|
||||||
|
this.destroy();
|
||||||
|
this.config.onTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 销毁实例 */
|
||||||
|
public destroy() {
|
||||||
|
this.config.events.forEach((event) => {
|
||||||
|
window.removeEventListener(event, this.debouncedReset);
|
||||||
|
});
|
||||||
|
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
||||||
|
|
||||||
|
if (this.timerId !== null) {
|
||||||
|
window.clearInterval(this.timerId);
|
||||||
|
this.timerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 超时时执行 */
|
||||||
|
private timeOutExec() {
|
||||||
|
// 移除 app 元素,即清空主界面
|
||||||
|
const appEl = document.getElementById('app')!;
|
||||||
|
appEl?.remove();
|
||||||
|
// 关闭连接
|
||||||
|
signalR.stop();
|
||||||
|
|
||||||
|
// TODO: 如果要改成调用 logout 登出接口,需要调整 clearAccessTokens 会 reload 页面的问题
|
||||||
|
|
||||||
|
// 清除 token
|
||||||
|
Local.remove(accessTokenKey);
|
||||||
|
Local.remove(refreshAccessTokenKey);
|
||||||
|
|
||||||
|
// 清除其他
|
||||||
|
Session.clear();
|
||||||
|
|
||||||
|
ElMessageBox.alert(i18n.global.t('message.list.idleTimeoutMessage'), i18n.global.t('message.list.sysMessage'), {
|
||||||
|
draggable: true,
|
||||||
|
callback: () => {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化函数(在应用启动时调用) */
|
||||||
|
export function initIdleTimeout(config?: IdleTimeoutConfig) {
|
||||||
|
// 确保单例模式
|
||||||
|
if (!window.__IDLE_TIMEOUT__) {
|
||||||
|
window.__IDLE_TIMEOUT__ = new IdleTimeoutManager(config);
|
||||||
|
}
|
||||||
|
return window.__IDLE_TIMEOUT__;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 销毁函数(在需要时调用) */
|
||||||
|
export function destroyIdleTimeout() {
|
||||||
|
if (window.__IDLE_TIMEOUT__) {
|
||||||
|
window.__IDLE_TIMEOUT__.destroy();
|
||||||
|
window.__IDLE_TIMEOUT__ = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新空闲超时时间(毫秒) */
|
||||||
|
export function updateIdleTimeout(timeout: number) {
|
||||||
|
if (window.__IDLE_TIMEOUT__) {
|
||||||
|
window.__IDLE_TIMEOUT__.updateIdleTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型扩展
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__IDLE_TIMEOUT__?: IdleTimeoutManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import logoImg from '/@/assets/logo.png';
|
|||||||
|
|
||||||
import { SysTenantApi } from '/@/api-services';
|
import { SysTenantApi } from '/@/api-services';
|
||||||
import { feature, getAPI } from '/@/utils/axios-utils';
|
import { feature, getAPI } from '/@/utils/axios-utils';
|
||||||
|
import { updateIdleTimeout } from './idleTimeout';
|
||||||
|
|
||||||
const storesThemeConfig = useThemeConfig();
|
const storesThemeConfig = useThemeConfig();
|
||||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||||
@ -56,12 +57,17 @@ export async function loadSysInfo(tenantid: number) {
|
|||||||
themeConfig.value.passwordExpirationTime = data.passwordExpirationTime;
|
themeConfig.value.passwordExpirationTime = data.passwordExpirationTime;
|
||||||
// 开启多语言切换
|
// 开启多语言切换
|
||||||
themeConfig.value.i18NSwitch = data.i18NSwitch;
|
themeConfig.value.i18NSwitch = data.i18NSwitch;
|
||||||
|
// 闲置超时时间
|
||||||
|
themeConfig.value.idleTimeout = data.idleTimeout;
|
||||||
// 密码加解密公匙
|
// 密码加解密公匙
|
||||||
window.__env__.VITE_SM_PUBLIC_KEY = data.publicKey;
|
window.__env__.VITE_SM_PUBLIC_KEY = data.publicKey;
|
||||||
|
|
||||||
// 更新 favicon
|
// 更新 favicon
|
||||||
updateFavicon(data.logo);
|
updateFavicon(data.logo);
|
||||||
|
|
||||||
|
// 更新空闲超时时间
|
||||||
|
updateIdleTimeout(themeConfig.value.idleTimeout ?? 0);
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
Local.remove('themeConfig');
|
Local.remove('themeConfig');
|
||||||
Local.set('themeConfig', storesThemeConfig.themeConfig);
|
Local.set('themeConfig', storesThemeConfig.themeConfig);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user