diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IIdentitySessionRepository.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IIdentitySessionRepository.cs index e8a413ffe..5b0e04d51 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IIdentitySessionRepository.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IIdentitySessionRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Volo.Abp.Identity; +using Volo.Abp.Specifications; namespace LINGYUN.Abp.Identity; public interface IIdentitySessionRepository : Volo.Abp.Identity.IIdentitySessionRepository @@ -22,4 +23,15 @@ public interface IIdentitySessionRepository : Volo.Abp.Identity.IIdentitySession Task> GetListAsync(Guid userId, CancellationToken cancellationToken = default); Task DeleteAllSessionAsync(string sessionId, Guid? exceptSessionId = null, CancellationToken cancellationToken = default); + + Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + ISpecification specification, + string sorting = $"{nameof(IdentitySession.SignedIn)} DESC", + int maxResultCount = 10, + int skipCount = 0, + CancellationToken cancellationToken = default); } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionStore.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionStore.cs index 4a4596e8d..5e76c2704 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionStore.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionStore.cs @@ -22,6 +22,7 @@ public interface IIdentitySessionStore /// 登录时间 /// 上次访问时间 /// IP地域 + /// 用户名称 /// 租户id /// /// 创建完成的 @@ -35,6 +36,7 @@ public interface IIdentitySessionStore DateTime signedIn, DateTime? lastAccessed = null, string ipRegion = null, + string userName = null, Guid? tenantId = null, CancellationToken cancellationToken = default); /// diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionCacheItemSynchronizer.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionCacheItemSynchronizer.cs index 1650e7730..65a261094 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionCacheItemSynchronizer.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionCacheItemSynchronizer.cs @@ -1,42 +1,31 @@ -using LINGYUN.Abp.Identity.Settings; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using System; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.DistributedLocking; -using Volo.Abp.Domain.Entities.Events; using Volo.Abp.Domain.Entities.Events.Distributed; -using Volo.Abp.EventBus; using Volo.Abp.EventBus.Distributed; -using Volo.Abp.Identity; using Volo.Abp.Settings; -using Volo.Abp.Uow; namespace LINGYUN.Abp.Identity.Session; public class IdentitySessionCacheItemSynchronizer : IDistributedEventHandler>, IDistributedEventHandler>, - IDistributedEventHandler, - ILocalEventHandler>, ITransientDependency { public ILogger Logger { protected get; set; } protected ISettingProvider SettingProvider { get; } protected IAbpDistributedLock DistributedLock { get; } protected IIdentitySessionCache IdentitySessionCache { get; } - protected IIdentitySessionStore IdentitySessionStore { get; } public IdentitySessionCacheItemSynchronizer( ISettingProvider settingProvider, IAbpDistributedLock distributedLock, - IIdentitySessionCache identitySessionCache, - IIdentitySessionStore identitySessionStore) + IIdentitySessionCache identitySessionCache) { SettingProvider = settingProvider; DistributedLock = distributedLock; IdentitySessionCache = identitySessionCache; - IdentitySessionStore = identitySessionStore; Logger = NullLogger.Instance; } @@ -46,7 +35,6 @@ public class IdentitySessionCacheItemSynchronizer : await IdentitySessionCache.RemoveAsync(eventData.Entity.SessionId); } - [UnitOfWork] public async virtual Task HandleEventAsync(EntityCreatedEto eventData) { var lockKey = $"{nameof(IdentitySessionCacheItemSynchronizer)}_{nameof(EntityCreatedEto)}"; @@ -61,58 +49,6 @@ public class IdentitySessionCacheItemSynchronizer : } await RefreshSessionCache(eventData.Entity); - await CheckConcurrentLoginStrategy(eventData.Entity); - } - } - - [UnitOfWork] - public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData) - { - var lockKey = $"{nameof(IdentitySessionCacheItemSynchronizer)}_{nameof(IdentitySessionChangeAccessedEvent)}"; - await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) - { - Logger.LogInformation($"Lock is acquired for {lockKey}"); - - if (handle == null) - { - Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); - return; - } - - var idetitySession = await IdentitySessionStore.FindAsync(eventData.SessionId); - if (idetitySession != null) - { - if (!eventData.IpAddresses.IsNullOrWhiteSpace()) - { - idetitySession.SetIpAddresses(eventData.IpAddresses.Split(",")); - } - idetitySession.UpdateLastAccessedTime(eventData.LastAccessed); - - await IdentitySessionStore.UpdateAsync(idetitySession); - } - else - { - // 数据库中不存在会话, 清理缓存, 后续请求会话失效 - await IdentitySessionCache.RemoveAsync(eventData.SessionId); - } - } - } - - public async virtual Task HandleEventAsync(EntityDeletedEventData eventData) - { - var lockKey = $"{nameof(IdentitySessionCacheItemSynchronizer)}_{nameof(EntityDeletedEventData)}"; - await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) - { - Logger.LogInformation($"Lock is acquired for {lockKey}"); - - if (handle == null) - { - Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); - return; - } - - // 用户被删除, 移除所有会话 - await IdentitySessionStore.RevokeAllAsync(eventData.Entity.Id); } } @@ -132,51 +68,4 @@ public class IdentitySessionCacheItemSynchronizer : session.SessionId, identitySessionCacheItem); } - - protected async virtual Task CheckConcurrentLoginStrategy(IdentitySessionEto session) - { - // 创建一个会话后根据策略使其他会话失效 - var strategySet = await SettingProvider.GetOrNullAsync(IdentitySettingNames.Session.ConcurrentLoginStrategy); - - Logger.LogDebug($"The concurrent login strategy is: {strategySet}"); - - if (!strategySet.IsNullOrWhiteSpace() && Enum.TryParse(strategySet, true, out var strategy)) - { - switch (strategy) - { - // 限制用户相同设备 - case ConcurrentLoginStrategy.LogoutFromSameTypeDevicesLimit: - - var sameTypeDevicesCountSet = await SettingProvider.GetAsync(IdentitySettingNames.Session.LogoutFromSameTypeDevicesLimit, 1); - - Logger.LogDebug($"Clear other sessions on the device {session.Device} and save only {sameTypeDevicesCountSet} sessions."); - - await IdentitySessionStore.RevokeWithAsync( - session.UserId, - session.Device, - session.Id, - sameTypeDevicesCountSet); - break; - // 限制登录设备 - case ConcurrentLoginStrategy.LogoutFromSameTypeDevices: - - Logger.LogDebug($"Clear all other sessions on the device {session.Device}."); - - await IdentitySessionStore.RevokeAllAsync( - session.UserId, - session.Device, - session.Id); - break; - // 限制多端登录 - case ConcurrentLoginStrategy.LogoutFromAllDevices: - - Logger.LogDebug($"Clear all other user sessions."); - - await IdentitySessionStore.RevokeAllAsync( - session.UserId, - session.Id); - break; - } - } - } } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionManager.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionManager.cs index c4db147aa..e365ad9f2 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionManager.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionManager.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Volo.Abp.Auditing; using Volo.Abp.Domain.Services; using Volo.Abp.Identity; +using Volo.Abp.Security.Claims; namespace LINGYUN.Abp.Identity.Session; public class IdentitySessionManager : DomainService, IIdentitySessionManager @@ -54,6 +55,7 @@ public class IdentitySessionManager : DomainService, IIdentitySessionManager var device = deviceInfo.Device ?? IdentitySessionDevices.OAuth; var deviceDesc = deviceInfo.Description; var clientIpAddress = deviceInfo.ClientIpAddress; + var userName = claimsPrincipal.FindFirstValue(AbpClaimTypes.UserName); var clientId = claimsPrincipal.FindClientId(); @@ -69,6 +71,7 @@ public class IdentitySessionManager : DomainService, IIdentitySessionManager Clock.Now, Clock.Now, deviceInfo.IpRegion, + userName, tenantId, cancellationToken); diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionStore.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionStore.cs index ed127f7d9..7a9b29632 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionStore.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionStore.cs @@ -37,6 +37,7 @@ public class IdentitySessionStore : IIdentitySessionStore, ITransientDependency DateTime signedIn, DateTime? lastAccessed = null, string ipRegion = null, + string userName = null, Guid? tenantId = null, CancellationToken cancellationToken = default) { @@ -55,10 +56,15 @@ public class IdentitySessionStore : IIdentitySessionStore, ITransientDependency signedIn, lastAccessed ); + if (!ipRegion.IsNullOrWhiteSpace()) { identitySession.SetProperty("Location", ipRegion); } + if (!userName.IsNullOrWhiteSpace()) + { + identitySession.SetProperty("UserName", userName); + } identitySession = await IdentitySessionRepository.InsertAsync(identitySession, cancellationToken: cancellationToken); diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN/Abp/Identity/EntityFrameworkCore/EfCoreIdentitySessionRepository.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN/Abp/Identity/EntityFrameworkCore/EfCoreIdentitySessionRepository.cs index 49e9b0b49..973019971 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN/Abp/Identity/EntityFrameworkCore/EfCoreIdentitySessionRepository.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN/Abp/Identity/EntityFrameworkCore/EfCoreIdentitySessionRepository.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Identity; using Volo.Abp.Identity.EntityFrameworkCore; +using Volo.Abp.Specifications; namespace LINGYUN.Abp.Identity.EntityFrameworkCore; @@ -59,4 +60,27 @@ public class EfCoreIdentitySessionRepository : Volo.Abp.Identity.EntityFramework { await DeleteAsync(x => x.SessionId == sessionId && x.Id != exceptSessionId, cancellationToken: cancellationToken); } + + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + return await (await GetDbSetAsync()) + .Where(specification.ToExpression()) + .CountAsync(GetCancellationToken(cancellationToken)); + } + + public async virtual Task> GetListAsync( + ISpecification specification, + string sorting = $"{nameof(IdentitySession.SignedIn)} DESC", + int maxResultCount = 10, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + return await (await GetDbSetAsync()) + .Where(specification.ToExpression()) + .OrderBy(!sorting.IsNullOrWhiteSpace() ? sorting : $"{nameof(IdentitySession.SignedIn)} DESC") + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationDefinitionProvider.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationDefinitionProvider.cs index 0d905cad6..0bcfe88ff 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationDefinitionProvider.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationDefinitionProvider.cs @@ -19,7 +19,20 @@ public class IdentityNotificationDefinitionProvider : NotificationDefinitionProv NotificationType.ServiceCallback, NotificationLifetime.Persistent, NotificationContentType.Json, - allowSubscriptionToClients: true); + allowSubscriptionToClients: false) // 客户端禁用, 所有新用户必须订阅此通知 + .WithProviders( + NotificationProviderNames.SignalR, // 实时通知处理会话过期事件 + NotificationProviderNames.Emailing); // 邮件通知会话过期 + + group.AddNotification( + IdentityNotificationNames.IdentityUser.CleaningUpInactiveUsers, + L("Notifications:CleaningUpInactiveUsers"), + L("Notifications:CleaningUpInactiveUsers"), + NotificationType.Application, + NotificationLifetime.Persistent, + NotificationContentType.Markdown, + allowSubscriptionToClients: false) // 客户端禁用, 所有新用户必须订阅此通知 + .WithProviders(NotificationProviderNames.Emailing); } private static ILocalizableString L(string name) diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationNames.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationNames.cs index 3614c4935..3ee5aed89 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationNames.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationNames.cs @@ -11,4 +11,13 @@ public static class IdentityNotificationNames /// public const string ExpirationSession = Prefix + ".Expiration"; } + + public static class IdentityUser + { + public const string Prefix = GroupName + ".IdentityUser"; + /// + /// 不活跃用户清理通知 + /// + public const string CleaningUpInactiveUsers = Prefix + ".CleaningUpInactiveUsers"; + } } diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xml b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xsd b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN.Abp.Identity.Jobs.csproj b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN.Abp.Identity.Jobs.csproj new file mode 100644 index 000000000..5bb62d4ca --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN.Abp.Identity.Jobs.csproj @@ -0,0 +1,27 @@ + + + + + + + net9.0 + LINGYUN.Abp.Identity.Jobs + LINGYUN.Abp.Identity.Jobs + false + false + false + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/AbpIdentityJobsModule.cs b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/AbpIdentityJobsModule.cs new file mode 100644 index 000000000..518b0f3df --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/AbpIdentityJobsModule.cs @@ -0,0 +1,29 @@ +using LINGYUN.Abp.BackgroundTasks; +using LINGYUN.Abp.Identity.Notifications; +using Volo.Abp.Identity.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.Identity.Jobs; + +[DependsOn(typeof(AbpIdentityDomainModule))] +[DependsOn(typeof(AbpIdentityNotificationsModule))] +[DependsOn(typeof(AbpBackgroundTasksAbstractionsModule))] +public class AbpIdentityJobsModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Add() + .AddVirtualJson("/LINGYUN/Abp/Identity/Jobs/Localization/Resources"); + }); + } +} diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/IdentityJobDefinitionProvider.cs b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/IdentityJobDefinitionProvider.cs new file mode 100644 index 000000000..8b65287e8 --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/IdentityJobDefinitionProvider.cs @@ -0,0 +1,21 @@ +using LINGYUN.Abp.BackgroundTasks; + +namespace LINGYUN.Abp.Identity.Jobs; +public class IdentityJobDefinitionProvider : JobDefinitionProvider +{ + public override void Define(IJobDefinitionContext context) + { + context.Add( + new JobDefinition( + InactiveIdentitySessionCleanupJob.Name, + typeof(InactiveIdentitySessionCleanupJob), + LocalizableStatic.Create("InactiveIdentitySessionCleanupJob"), + InactiveIdentitySessionCleanupJob.Paramters) + // TODO: 实现用户过期清理作业需要增加用户会话实体 + //new JobDefinition( + // InactiveIdentityUserCleanupJob.Name, + // typeof(InactiveIdentityUserCleanupJob), + // LocalizableStatic.Create("InactiveIdentityUserCleanupJob")) + ); + } +} diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/InactiveIdentitySessionCleanupJob.cs b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/InactiveIdentitySessionCleanupJob.cs new file mode 100644 index 000000000..7c2aa50af --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/InactiveIdentitySessionCleanupJob.cs @@ -0,0 +1,49 @@ +using LINGYUN.Abp.BackgroundTasks; +using LINGYUN.Abp.Identity.Session; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.Identity.Jobs; +/// +/// 用户会话清理作业 +/// +/// +/// 此作业启用时,建议禁用 +/// +public class InactiveIdentitySessionCleanupJob : IJobRunnable +{ + public const string Name = "InactiveIdentitySessionCleanupJob"; + + #region Definition Paramters + + public readonly static IReadOnlyList Paramters = + new List + { + new JobDefinitionParamter( + PropertySessionInactiveDays, + LocalizableStatic.Create("DisplayName:SessionInactiveDays"), + LocalizableStatic.Create("Description:SessionInactiveDays")) + }; + + #endregion + /// + /// 不活跃会话保持时长, 单位天 + /// + public const string PropertySessionInactiveDays = "SessionInactiveDays"; + + public async virtual Task ExecuteAsync(JobRunnableContext context) + { + var logger = context.GetRequiredService>(); + var sessionStore = context.GetRequiredService(); + + var inactiveDays = context.GetOrDefaultJobData(PropertySessionInactiveDays, 30); + + logger.LogInformation("Prepare to clean up sessions that have been inactive for more than {inactiveDays} days.", inactiveDays); + + await sessionStore.RevokeAllAsync(TimeSpan.FromDays(inactiveDays)); + + logger.LogInformation($"Cleaned inactive user session."); + } +} diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/InactiveIdentityUserCleanupJob.cs b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/InactiveIdentityUserCleanupJob.cs new file mode 100644 index 000000000..c577f18d0 --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/InactiveIdentityUserCleanupJob.cs @@ -0,0 +1,125 @@ +//using LINGYUN.Abp.BackgroundTasks; +//using LINGYUN.Abp.Identity.Notifications; +//using LINGYUN.Abp.Notifications; +//using Microsoft.Extensions.Logging; +//using System.Collections.Generic; +//using System.Linq; +//using System.Threading.Tasks; +//using Volo.Abp.Domain.Repositories; +//using Volo.Abp.Identity; +//using Volo.Abp.Specifications; +//using Volo.Abp.Timing; + +//namespace LINGYUN.Abp.Identity.Jobs; + +///// +///// 用户清理作业 +///// +///// +///// 清理长期未登录用户 +///// +//public class InactiveIdentityUserCleanupJob : IJobRunnable +//{ +// public const string Name = "InactiveIdentityUserCleanupJob"; + +// #region Definition Paramters + +// public readonly static IReadOnlyList Paramters = +// new List +// { +// new JobDefinitionParamter( +// PropertyUserInactiveCleanupDays, +// LocalizableStatic.Create("DisplayName:UserInactiveCleanupDays"), +// LocalizableStatic.Create("Description:UserInactiveCleanupDays")), +// new JobDefinitionParamter( +// PropertyUserInactiveNotifierDays, +// LocalizableStatic.Create("DisplayName:UserInactiveNotifierDays"), +// LocalizableStatic.Create("Description:UserInactiveNotifierDays")), +// }; + +// #endregion +// /// +// /// 不活跃用户清理时间, 单位: 天 +// /// +// public const string PropertyUserInactiveCleanupDays = "UserInactiveCleanupDays"; +// /// +// /// 不活跃用户通知时间, 单位: 天 +// /// +// public const string PropertyUserInactiveNotifierDays = "UserInactiveNotifierDays"; + +// public async virtual Task ExecuteAsync(JobRunnableContext context) +// { +// var logger = context.GetRequiredService>(); + +// // 不活跃用户清理时间 +// var inactiveCleanupDays = context.GetOrDefaultJobData(PropertyUserInactiveCleanupDays, 30); +// // 不活跃用户通知时间 +// var inactiveNotifierDays = context.GetOrDefaultJobData(PropertyUserInactiveNotifierDays, 180); + +// var clock = context.GetRequiredService(); +// var identityUserRepo = context.GetRequiredService(); +// var identityUserSessionRepo = context.GetRequiredService(); + +// // 获取需要清理的用户集合 +// var specification = new ExpressionSpecification( +// x => x.LastAccessed <= clock.Now.AddDays(-inactiveNotifierDays)); + +// using (identityUserSessionRepo.DisableTracking()) +// { +// var inactiveUserCount = await identityUserSessionRepo.GetCountAsync(specification); +// if (inactiveUserCount == 0) +// { +// logger.LogInformation("There are no inactive users to be notified."); +// return; +// } +// // 不活跃用户会话集合 +// var inactiveUsers = await identityUserSessionRepo.GetListAsync(specification, maxResultCount: inactiveUserCount); + +// // 直接清理的不活跃用户集合 +// var inactiveCleanupUsers = inactiveUsers.Where(x => x.LastAccessed <= clock.Now.AddDays(-inactiveCleanupDays)); + +// // 需要通知的不活跃用户 +// var inactiveNotifierUsers = inactiveUsers.ExceptBy(inactiveCleanupUsers.Select(x => x.Id), x => x.Id); +// if (inactiveNotifierUsers.Count() != 0) +// { +// await SendInactiveUserNotifier(context, inactiveNotifierUsers); +// } + +// if (inactiveCleanupUsers.Count() != 0) +// { +// logger.LogInformation( +// "Prepare to clean up {count} users who have been inactive for more than {inactiveCleanupDays} days.", +// inactiveCleanupUsers.Count(), +// inactiveCleanupDays); + +// // 清理不活跃用户 +// await identityUserRepo.DeleteManyAsync(inactiveCleanupUsers.Select(x => x.UserId)); +// } +// } +// logger.LogInformation($"Cleaned inactive users."); +// } + +// /// +// /// 发送不活跃用户清理通知 +// /// +// /// +// /// +// /// +// private async Task SendInactiveUserNotifier(JobRunnableContext context, IEnumerable userSessions) +// { +// // TODO: 完成不活跃用户清理通知 +// var notificationSender = context.GetRequiredService(); + +// var notificationTemplate = new NotificationTemplate( +// IdentityNotificationNames.IdentityUser.CleaningUpInactiveUsers, +// data: new Dictionary +// { + +// }); + +// await notificationSender.SendNofiterAsync( +// IdentityNotificationNames.IdentityUser.CleaningUpInactiveUsers, +// notificationTemplate +// ); +// } +//} diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/LocalizableStatic.cs b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/LocalizableStatic.cs new file mode 100644 index 000000000..12d1be750 --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/LocalizableStatic.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Identity.Localization; +using Volo.Abp.Localization; + +namespace LINGYUN.Abp.Identity.Jobs; + +internal static class LocalizableStatic +{ + public static ILocalizableString Create(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/Localization/Resources/en.json b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/Localization/Resources/en.json new file mode 100644 index 000000000..97e971ee2 --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/Localization/Resources/en.json @@ -0,0 +1,9 @@ +{ + "culture": "en", + "texts": { + "InactiveIdentitySessionCleanupJob": "Inactive Session Cleanup Job", + "InactiveIdentityUserCleanupJob": "Inactive User Cleanup Job", + "DisplayName:SessionInactiveDays": "Duration of inactive conversation", + "Description:SessionInactiveDays": "Duration of inactive conversation, unit: days." + } +} \ No newline at end of file diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/Localization/Resources/zh-Hans.json b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..3fd0242bd --- /dev/null +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/Localization/Resources/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "culture": "zh-Hans", + "texts": { + "InactiveIdentitySessionCleanupJob": "不活跃会话清理作业", + "InactiveIdentityUserCleanupJob": "不活跃用户清理作业", + "DisplayName:SessionInactiveDays": "不活跃会话保持时长", + "Description:SessionInactiveDays": "不活跃会话保持时长,单位: 天." + } +} \ No newline at end of file diff --git a/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs b/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs new file mode 100644 index 000000000..9de763da8 --- /dev/null +++ b/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs @@ -0,0 +1,164 @@ +using LINGYUN.Abp.Identity; +using LINGYUN.Abp.Identity.Session; +using LINGYUN.Abp.Identity.Settings; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Settings; +using Volo.Abp.Uow; +using Volo.Abp.Users; + +namespace LY.MicroService.AuthServer.Handlers; +/// +/// 会话控制事件处理器 +/// +public class IdentitySessionAccessEventHandler : + IDistributedEventHandler, + IDistributedEventHandler>, + IDistributedEventHandler>, + ITransientDependency +{ + public ILogger Logger { protected get; set; } + protected ISettingProvider SettingProvider { get; } + protected IAbpDistributedLock DistributedLock { get; } + protected IIdentitySessionCache IdentitySessionCache { get; } + protected IIdentitySessionStore IdentitySessionStore { get; } + + public IdentitySessionAccessEventHandler( + ISettingProvider settingProvider, + IAbpDistributedLock distributedLock, + IIdentitySessionCache identitySessionCache, + IIdentitySessionStore identitySessionStore) + { + SettingProvider = settingProvider; + DistributedLock = distributedLock; + IdentitySessionCache = identitySessionCache; + IdentitySessionStore = identitySessionStore; + + Logger = NullLogger.Instance; + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(EntityCreatedEto eventData) + { + // 新会话创建时检查登录策略 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto)}"; + await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) + { + Logger.LogInformation($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + return; + } + + await CheckConcurrentLoginStrategy(eventData.Entity); + } + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(EntityDeletedEto eventData) + { + // 用户被删除, 移除所有会话 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto)}"; + await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) + { + Logger.LogInformation($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + return; + } + + await IdentitySessionStore.RevokeAllAsync(eventData.Entity.Id); + } + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData) + { + // 会话访问更新 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentitySessionChangeAccessedEvent)}"; + await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) + { + Logger.LogInformation($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + return; + } + + var idetitySession = await IdentitySessionStore.FindAsync(eventData.SessionId); + if (idetitySession != null) + { + if (!eventData.IpAddresses.IsNullOrWhiteSpace()) + { + idetitySession.SetIpAddresses(eventData.IpAddresses.Split(",")); + } + idetitySession.UpdateLastAccessedTime(eventData.LastAccessed); + + await IdentitySessionStore.UpdateAsync(idetitySession); + } + else + { + // 数据库中不存在会话, 清理缓存, 后续请求会话失效 + await IdentitySessionCache.RemoveAsync(eventData.SessionId); + } + } + } + + protected async virtual Task CheckConcurrentLoginStrategy(IdentitySessionEto session) + { + // 创建一个会话后根据策略使其他会话失效 + var strategySet = await SettingProvider.GetOrNullAsync(IdentitySettingNames.Session.ConcurrentLoginStrategy); + + Logger.LogDebug($"The concurrent login strategy is: {strategySet}"); + + if (!strategySet.IsNullOrWhiteSpace() && Enum.TryParse(strategySet, true, out var strategy)) + { + switch (strategy) + { + // 限制用户相同设备 + case ConcurrentLoginStrategy.LogoutFromSameTypeDevicesLimit: + + var sameTypeDevicesCountSet = await SettingProvider.GetAsync(IdentitySettingNames.Session.LogoutFromSameTypeDevicesLimit, 1); + + Logger.LogDebug($"Clear other sessions on the device {session.Device} and save only {sameTypeDevicesCountSet} sessions."); + + await IdentitySessionStore.RevokeWithAsync( + session.UserId, + session.Device, + session.Id, + sameTypeDevicesCountSet); + break; + // 限制登录设备 + case ConcurrentLoginStrategy.LogoutFromSameTypeDevices: + + Logger.LogDebug($"Clear all other sessions on the device {session.Device}."); + + await IdentitySessionStore.RevokeAllAsync( + session.UserId, + session.Device, + session.Id); + break; + // 限制多端登录 + case ConcurrentLoginStrategy.LogoutFromAllDevices: + + Logger.LogDebug($"Clear all other user sessions."); + + await IdentitySessionStore.RevokeAllAsync( + session.UserId, + session.Id); + break; + } + } + } +} diff --git a/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs b/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs new file mode 100644 index 000000000..4dc22a3fe --- /dev/null +++ b/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs @@ -0,0 +1,164 @@ +using LINGYUN.Abp.Identity; +using LINGYUN.Abp.Identity.Session; +using LINGYUN.Abp.Identity.Settings; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Settings; +using Volo.Abp.Uow; +using Volo.Abp.Users; + +namespace LY.MicroService.IdentityServer.Handlers; +/// +/// 会话控制事件处理器 +/// +public class IdentitySessionAccessEventHandler : + IDistributedEventHandler, + IDistributedEventHandler>, + IDistributedEventHandler>, + ITransientDependency +{ + public ILogger Logger { protected get; set; } + protected ISettingProvider SettingProvider { get; } + protected IAbpDistributedLock DistributedLock { get; } + protected IIdentitySessionCache IdentitySessionCache { get; } + protected IIdentitySessionStore IdentitySessionStore { get; } + + public IdentitySessionAccessEventHandler( + ISettingProvider settingProvider, + IAbpDistributedLock distributedLock, + IIdentitySessionCache identitySessionCache, + IIdentitySessionStore identitySessionStore) + { + SettingProvider = settingProvider; + DistributedLock = distributedLock; + IdentitySessionCache = identitySessionCache; + IdentitySessionStore = identitySessionStore; + + Logger = NullLogger.Instance; + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(EntityCreatedEto eventData) + { + // 新会话创建时检查登录策略 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto)}"; + await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) + { + Logger.LogInformation($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + return; + } + + await CheckConcurrentLoginStrategy(eventData.Entity); + } + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(EntityDeletedEto eventData) + { + // 用户被删除, 移除所有会话 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto)}"; + await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) + { + Logger.LogInformation($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + return; + } + + await IdentitySessionStore.RevokeAllAsync(eventData.Entity.Id); + } + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData) + { + // 会话访问更新 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentitySessionChangeAccessedEvent)}"; + await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) + { + Logger.LogInformation($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + return; + } + + var idetitySession = await IdentitySessionStore.FindAsync(eventData.SessionId); + if (idetitySession != null) + { + if (!eventData.IpAddresses.IsNullOrWhiteSpace()) + { + idetitySession.SetIpAddresses(eventData.IpAddresses.Split(",")); + } + idetitySession.UpdateLastAccessedTime(eventData.LastAccessed); + + await IdentitySessionStore.UpdateAsync(idetitySession); + } + else + { + // 数据库中不存在会话, 清理缓存, 后续请求会话失效 + await IdentitySessionCache.RemoveAsync(eventData.SessionId); + } + } + } + + protected async virtual Task CheckConcurrentLoginStrategy(IdentitySessionEto session) + { + // 创建一个会话后根据策略使其他会话失效 + var strategySet = await SettingProvider.GetOrNullAsync(IdentitySettingNames.Session.ConcurrentLoginStrategy); + + Logger.LogDebug($"The concurrent login strategy is: {strategySet}"); + + if (!strategySet.IsNullOrWhiteSpace() && Enum.TryParse(strategySet, true, out var strategy)) + { + switch (strategy) + { + // 限制用户相同设备 + case ConcurrentLoginStrategy.LogoutFromSameTypeDevicesLimit: + + var sameTypeDevicesCountSet = await SettingProvider.GetAsync(IdentitySettingNames.Session.LogoutFromSameTypeDevicesLimit, 1); + + Logger.LogDebug($"Clear other sessions on the device {session.Device} and save only {sameTypeDevicesCountSet} sessions."); + + await IdentitySessionStore.RevokeWithAsync( + session.UserId, + session.Device, + session.Id, + sameTypeDevicesCountSet); + break; + // 限制登录设备 + case ConcurrentLoginStrategy.LogoutFromSameTypeDevices: + + Logger.LogDebug($"Clear all other sessions on the device {session.Device}."); + + await IdentitySessionStore.RevokeAllAsync( + session.UserId, + session.Device, + session.Id); + break; + // 限制多端登录 + case ConcurrentLoginStrategy.LogoutFromAllDevices: + + Logger.LogDebug($"Clear all other user sessions."); + + await IdentitySessionStore.RevokeAllAsync( + session.UserId, + session.Id); + break; + } + } + } +} diff --git a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/EventBus/Local/UserSubscribeSessionExpirationEventHandler.cs b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/EventBus/Local/UserSubscribeSessionExpirationEventHandler.cs index 870805fba..cc2deae80 100644 --- a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/EventBus/Local/UserSubscribeSessionExpirationEventHandler.cs +++ b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/EventBus/Local/UserSubscribeSessionExpirationEventHandler.cs @@ -19,10 +19,17 @@ public class UserSubscribeSessionExpirationEventHandler : ILocalEventHandler eventData) { + // 新用户订阅会话过期通知 await _notificationSubscriptionManager .SubscribeAsync( eventData.Entity.TenantId, new UserIdentifier(eventData.Entity.Id, eventData.Entity.UserName), IdentityNotificationNames.Session.ExpirationSession); + // 新用户订阅不活跃用户清理通知 + await _notificationSubscriptionManager + .SubscribeAsync( + eventData.Entity.TenantId, + new UserIdentifier(eventData.Entity.Id, eventData.Entity.UserName), + IdentityNotificationNames.IdentityUser.CleaningUpInactiveUsers); } }