如何实现Abp Vnext商业版的Idle功能

2025-06-22  本文已影响0人  ArcherTrister

今天发现商业版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

  };

}

```

上一篇 下一篇

猜你喜欢

热点阅读