如何实现Abp Vnext商业版的Idle功能
今天发现商业版demo增加了一个空闲会话超时的功能,那就让我们来实现一下。
一、老规矩,必要的地址先奉上
1. https://demo.abp.io/index.html
2. https://abp.io/docs/api/9.2/index.html
3. https://abp.io/docs/latest/modules/account/idle-session-timeout
二、打开 Abp Vnext Demo 找到Idle功能页面
这时我们能够看到一段很显眼的蓝色文字【了解有关空闲会话超时的更多信息】,既然让我们了解那肯定必须充分了解一下了。
充分了解后,总结一下关键点
1. 有个空闲会话超时开关
2. 当开启时能够设置空闲会话超时时间
3. 如果登录时勾选了记住我,即使打开了开关也不会过期,反之则弹出过期倒计时对话框
三、实际操作一下,拿到必要的接口地址、响应参数、请求参数
接口地址(Get/Put):https://demo.abp.io/api/account-admin/settings/idle
响应参数:{"enabled":false,"idleTimeoutMinutes":1}
请求参数:{"enabled":true,"idleTimeoutMinutes":1}
四、设计服务接口和WebApi
由接口地址可知路由为`[Route("api/account-admin/settings")]`
控制器名称为`AccountAdminSettingsController`或`AccountSettingsController`
服务接口名称为`IAccountAdminSettingsAppService`或`IAccountSettingsAppService`
服务名称为`AccountAdminSettingsAppService`或`AccountSettingsAppService`
方法为`GetIdleAsync`和`UpdateIdleAsync`,
由请求参数和响应参数可看出其属性完全一致,根据复用原则猜测应该是同一个Dto,再根据根据框架命名规则,可暂定为`AccountIdleSettingsDto`或`IdleSettingsDto`
五、确定最终的命名
此时由于出现了不确定的名称命名,下面给出两种解决方案
- 根据自己喜好选择其中一个名称,根据请求参数和响应参数完善Dto
- 根据Api文档确定名称
下面讲述如何通过Api文档确认名称
1. 访问 [文档](https://abp.io/docs/api/9.2/index.html),在左侧搜索框中输入关键词`Idle`进行搜索可得到下图所示内容
此时需在左侧滚动滚动条完整查看内容,找出我们所需内容,由于我们现在不涉及Blazor相关的内容,故将其排除,浏览完剩下搜索内容可得出结论:
1. Dto名称为`AccountIdleSettingsDto`
2. Idle设置名称常量的定义在`AccountSettingNames`类下的`Idle`
```csharp
public class AccountIdleSettingsDto
{
public bool Enabled { get; set; }
public int IdleTimeoutMinutes { get; set; }
}
public static class AccountSettingNames
{
public static class Idle
{
public const string Enabled = "Abp.Account.Idle.Enabled";
public const string IdleTimeoutMinutes = "Abp.Account.Idle.IdleTimeoutMinutes";
}
}
```
2. 继续在左侧搜索框中输入关键词`AccountAdminSettingsAppService`或`AccountSettingsAppService`再次进行搜索,可得到如下所示内容
浏览完搜索内容可得出结论:
1. 服务接口名称为`IAccountSettingsAppService`,服务名称为`AccountSettingsAppService`
```csharp
public interface IAccountSettingsAppService : IApplicationService, IRemoteService
{
...
Task<AccountIdleSettingsDto> GetIdleAsync();
Task UpdateIdleAsync(AccountIdleSettingsDto input);
}
[Authorize("AbpAccount.SettingManagement")]
public class AccountSettingsAppService : ApplicationService, IAvoidDuplicateCrossCuttingConcerns, IValidationEnabled, IUnitOfWorkEnabled, IAuditingEnabled, IGlobalFeatureCheckingEnabled, ITransientDependency, IAccountSettingsAppService, IApplicationService, IRemoteService
{
protected ISettingManager SettingManager { get; }
protected ExternalProviderSettingsHelper ExternalProviderSettingsHelper { get; }
public AccountSettingsAppService(ISettingManager settingManager,
ExternalProviderSettingsHelper externalProviderSettingsHelper)
{
SettingManager = settingManager;
ExternalProviderSettingsHelper = externalProviderSettingsHelper;
}
...
public virtual Task<AccountIdleSettingsDto> GetIdleAsync()
{
...
}
public virtual Task UpdateIdleAsync(AccountIdleSettingsDto input)
{
...
}
}
```
3. 继续在左侧搜索框中输入关键词`AccountAdminSettingsController`或`AccountSettingsController`再次进行搜索,可得到如下所示内容
浏览完搜索内容可得出结论:
1. 控制器名称为`AccountSettingsController`
```csharp
[RemoteService(true, Name = "AbpAccountAdmin")]
[Area("accountAdmin")]
[Route("api/account-admin/settings")]
public class AccountSettingsController : AbpController, IActionFilter, IAsyncActionFilter, IFilterMetadata, IDisposable, IAvoidDuplicateCrossCuttingConcerns, IAccountSettingsAppService, IApplicationService, IRemoteService
{
protected IAccountSettingsAppService AccountSettingsAppService { get; }
public AccountSettingsController(IAccountSettingsAppService accountSettingsAppService)
{
AccountSettingsAppService = accountSettingsAppService;
}
[HttpGet]
[Route("idle")]
public virtual Task<AccountIdleSettingsDto> GetIdleAsync()
{
...
}
[HttpPut]
[Route("idle")]
public virtual Task UpdateIdleAsync(AccountIdleSettingsDto input)
{
...
}
}
```
六、根据操作相关功能体验,实现接口,并删除冗余的继承接口
思考🤔:此页面位置的设置都跟租户有关,所以接口只需实现租户对应的设置获取和更新即可
```csharp
public interface IAccountSettingsAppService : IApplicationService
{
...
Task<AccountIdleSettingsDto> GetIdleAsync();
Task UpdateIdleAsync(AccountIdleSettingsDto input);
}
[Authorize("AbpAccount.SettingManagement")]
public class AccountSettingsAppService : ApplicationService, IAccountSettingsAppService
{
protected ISettingManager SettingManager { get; }
protected ExternalProviderSettingsHelper ExternalProviderSettingsHelper { get; }
public AccountSettingsAppService(ISettingManager settingManager,
ExternalProviderSettingsHelper externalProviderSettingsHelper)
{
SettingManager = settingManager;
ExternalProviderSettingsHelper = externalProviderSettingsHelper;
}
...
public virtual async Task<AccountIdleSettingsDto> GetIdleAsync()
{
return new AccountIdleSettingsDto
{
Enabled = await SettingProvider.GetAsync<bool>(AccountSettingNames.Idle.Enabled),
IdleTimeoutMinutes = await SettingProvider.GetAsync<int>(AccountSettingNames.Idle.IdleTimeoutMinutes)
};
}
public virtual async Task UpdateIdleAsync(AccountIdleSettingsDto input)
{
if (input != null)
{
await SettingManager.SetForCurrentTenantAsync(AccountSettingNames.Idle.Enabled, input.Enabled.ToString());
await SettingManager.SetForCurrentTenantAsync(AccountSettingNames.Idle.IdleTimeoutMinutes, input.IdleTimeoutMinutes.ToString());
}
}
}
[RemoteService(true, Name = "AbpAccountAdmin")]
[Area("accountAdmin")]
[Route("api/account-admin/settings")]
public class AccountSettingsController : AbpController, IAccountSettingsAppService
{
protected IAccountSettingsAppService AccountSettingsAppService { get; }
public AccountSettingsController(IAccountSettingsAppService accountSettingsAppService)
{
AccountSettingsAppService = accountSettingsAppService;
}
[HttpGet]
[Route("idle")]
public virtual async Task<AccountIdleSettingsDto> GetIdleAsync()
{
return await AccountSettingsAppService.GetIdleAsync();
}
[HttpPut]
[Route("idle")]
public virtual async Task UpdateIdleAsync(AccountIdleSettingsDto input)
{
await AccountSettingsAppService.UpdateIdleAsync(input);
}
}
```
七、调试功能是否到达Demo的演示效果,然后根据调试结果做相应的调整
此处省略大约一万个字...
八、前端的IdleHooks代码,在App.vue中使用即可
```vue
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { useAuthStore } from '@/stores/auth';
// 事件类型常量
const USER_ACTIVITY_EVENTS = [
'click',
'keydown',
'mousemove',
'scroll',
'storage',
'touchstart',
'touchmove'
] as const;
export interface IdleSessionOptions {
isEnableIdle?: boolean; // 是否启用空闲会话功能,默认false
idleTimeout?: number; // 空闲超时时间(秒),默认300秒(5分钟)
timeoutWarningTime?: number; // 警告时间(秒),默认60秒(1分钟)
}
export function useIdleSession(options: IdleSessionOptions = {}) {
// 合并配置参数
const config = {
isEnableIdle: false,
idleTimeout: 300,
timeoutWarningTime: 60,
...options
};
const authStore = useAuthStore();
// 响应式状态
const isIdle = ref(false);
const warningCountdown = ref(0);
const showWarning = ref(false);
// 计时器引用
let idleTimer: number | null = null;
let warningTimer: number | null = null;
// 清除所有计时器(不重置状态)
const clearTimers = () => {
if (idleTimer !== null) {
window.clearTimeout(idleTimer);
idleTimer = null;
}
if (warningTimer !== null) {
window.clearInterval(warningTimer);
warningTimer = null;
}
};
// 重置所有状态和计时器
const resetAll = () => {
clearTimers();
isIdle.value = false;
showWarning.value = false;
warningCountdown.value = 0;
};
// 重置空闲计时器(完全重置)
const resetIdleTimer = () => {
console.info('重置空闲计时器');
resetAll();
if(!config.isEnableIdle) {
console.info('空闲会话功能已禁用');
return;
}else{
console.info('空闲会话功能已启用');
}
// 如果用户未登录,不启动计时器
if (!authStore.isAuthenticated) {
console.info('用户未登录,不启动计时器');
return;
}
// 如果用户设置了记住我,不启动计时器
if (authStore.isRemeberMe) {
console.info('用户已设置记住我,跳过警告倒计时');
return;
}else{
console.info('用户未设置记住我,启动空闲计时器');
}
// 设置新的空闲计时器
idleTimer = window.setTimeout(() => {
// 再次检查登录状态
if (!authStore.isAuthenticated) {
resetAll();
return;
}
isIdle.value = true;
startWarningCountdown();
}, config.idleTimeout * 1000);
};
// 重置警告倒计时(仅重置警告部分)
const resetWarningCountdown = () => {
clearTimers(); // 清除所有计时器
warningCountdown.value = config.timeoutWarningTime;
warningTimer = window.setInterval(() => {
warningCountdown.value -= 1;
if (warningCountdown.value <= 0) {
clearTimers();
logoutUser();
}
}, 1000);
};
// 开始警告倒计时
const startWarningCountdown = () => {
// 如果用户未登录,不显示警告
if (!authStore.isAuthenticated) {
resetAll();
return;
}
showWarning.value = true;
resetWarningCountdown();
};
// 登出用户
const logoutUser = () => {
resetAll(); // 登出时重置所有状态
authStore.logout();
};
// 用户活动检测
const trackUserActivity = () => {
// 如果用户未登录,不跟踪活动
if (!authStore.isAuthenticated) {
resetAll();
return;
}
// 在警告期间不重置倒计时
if (showWarning.value) {
return;
}
// 非空闲状态:重置整个计时器
if (!isIdle.value) {
resetIdleTimer();
}
};
// 初始化事件监听
onMounted(() => {
USER_ACTIVITY_EVENTS.forEach(event => {
window.addEventListener(event, trackUserActivity);
});
// 启动初始计时器(如果用户已登录)
if (authStore.isAuthenticated) {
resetIdleTimer();
}
});
// 监听登录状态变化
watch(
() => authStore.isAuthenticated,
(isLoggedIn) => {
if (isLoggedIn) {
// 用户登录时启动计时器
resetIdleTimer();
} else {
// 用户登出时重置所有状态
resetAll();
}
}
);
// 清理
onUnmounted(() => {
resetAll();
USER_ACTIVITY_EVENTS.forEach(event => {
window.removeEventListener(event, trackUserActivity);
});
});
return {
isIdle,
showWarning,
warningCountdown,
resetIdleTimer,
logoutUser
};
}
```