Browse Source

Merge pull request #1375 from colinin/refactor-identity-session

refactor(session): refactor identity session
pull/1380/head
yx lin 3 months ago
committed by GitHub
parent
commit
0356d50c9e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IIdentitySessionRepository.cs
  2. 2
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionStore.cs
  3. 115
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionCacheItemSynchronizer.cs
  4. 3
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionManager.cs
  5. 6
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionStore.cs
  6. 24
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN/Abp/Identity/EntityFrameworkCore/EfCoreIdentitySessionRepository.cs
  7. 15
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationDefinitionProvider.cs
  8. 9
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationNames.cs
  9. 3
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xml
  10. 30
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xsd
  11. 27
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN.Abp.Identity.Jobs.csproj
  12. 29
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/AbpIdentityJobsModule.cs
  13. 21
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/IdentityJobDefinitionProvider.cs
  14. 49
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/InactiveIdentitySessionCleanupJob.cs
  15. 125
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/InactiveIdentityUserCleanupJob.cs
  16. 12
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/LocalizableStatic.cs
  17. 9
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/Localization/Resources/en.json
  18. 9
      aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN/Abp/Identity/Jobs/Localization/Resources/zh-Hans.json
  19. 164
      aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs
  20. 164
      aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs
  21. 7
      aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/EventBus/Local/UserSubscribeSessionExpirationEventHandler.cs

12
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<List<IdentitySession>> GetListAsync(Guid userId, CancellationToken cancellationToken = default);
Task DeleteAllSessionAsync(string sessionId, Guid? exceptSessionId = null, CancellationToken cancellationToken = default);
Task<int> GetCountAsync(
ISpecification<IdentitySession> specification,
CancellationToken cancellationToken = default);
Task<List<IdentitySession>> GetListAsync(
ISpecification<IdentitySession> specification,
string sorting = $"{nameof(IdentitySession.SignedIn)} DESC",
int maxResultCount = 10,
int skipCount = 0,
CancellationToken cancellationToken = default);
}

2
aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionStore.cs

@ -22,6 +22,7 @@ public interface IIdentitySessionStore
/// <param name="signedIn">登录时间</param>
/// <param name="lastAccessed">上次访问时间</param>
/// <param name="ipRegion">IP地域</param>
/// <param name="userName">用户名称</param>
/// <param name="tenantId">租户id</param>
/// <param name="cancellationToken"></param>
/// <returns>创建完成的 <seealso cref="IdentitySession"/></returns>
@ -35,6 +36,7 @@ public interface IIdentitySessionStore
DateTime signedIn,
DateTime? lastAccessed = null,
string ipRegion = null,
string userName = null,
Guid? tenantId = null,
CancellationToken cancellationToken = default);
/// <summary>

115
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<EntityCreatedEto<IdentitySessionEto>>,
IDistributedEventHandler<EntityDeletedEto<IdentitySessionEto>>,
IDistributedEventHandler<IdentitySessionChangeAccessedEvent>,
ILocalEventHandler<EntityDeletedEventData<IdentityUser>>,
ITransientDependency
{
public ILogger<IdentitySessionCacheItemSynchronizer> 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<IdentitySessionCacheItemSynchronizer>.Instance;
}
@ -46,7 +35,6 @@ public class IdentitySessionCacheItemSynchronizer :
await IdentitySessionCache.RemoveAsync(eventData.Entity.SessionId);
}
[UnitOfWork]
public async virtual Task HandleEventAsync(EntityCreatedEto<IdentitySessionEto> eventData)
{
var lockKey = $"{nameof(IdentitySessionCacheItemSynchronizer)}_{nameof(EntityCreatedEto<IdentitySessionEto>)}";
@ -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<IdentityUser> eventData)
{
var lockKey = $"{nameof(IdentitySessionCacheItemSynchronizer)}_{nameof(EntityDeletedEventData<IdentityUser>)}";
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<ConcurrentLoginStrategy>(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;
}
}
}
}

3
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);

6
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);

24
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<int> GetCountAsync(
ISpecification<IdentitySession> specification,
CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.Where(specification.ToExpression())
.CountAsync(GetCancellationToken(cancellationToken));
}
public async virtual Task<List<IdentitySession>> GetListAsync(
ISpecification<IdentitySession> 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));
}
}

15
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)

9
aspnet-core/modules/identity/LINGYUN.Abp.Identity.Notifications/LINGYUN/Abp/Identity/Notifications/IdentityNotificationNames.cs

@ -11,4 +11,13 @@ public static class IdentityNotificationNames
/// </summary>
public const string ExpirationSession = Prefix + ".Expiration";
}
public static class IdentityUser
{
public const string Prefix = GroupName + ".IdentityUser";
/// <summary>
/// 不活跃用户清理通知
/// </summary>
public const string CleaningUpInactiveUsers = Prefix + ".CleaningUpInactiveUsers";
}
}

3
aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

27
aspnet-core/modules/task-management/LINGYUN.Abp.Identity.Jobs/LINGYUN.Abp.Identity.Jobs.csproj

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\configureawait.props" />
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AssemblyName>LINGYUN.Abp.Identity.Jobs</AssemblyName>
<PackageId>LINGYUN.Abp.Identity.Jobs</PackageId>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<None Remove="LINGYUN\Abp\Identity\Jobs\Localization\Resources\*.json" />
<EmbeddedResource Include="LINGYUN\Abp\Identity\Jobs\Localization\Resources\*.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\identity\LINGYUN.Abp.Identity.Domain\LINGYUN.Abp.Identity.Domain.csproj" />
<ProjectReference Include="..\..\identity\LINGYUN.Abp.Identity.Notifications\LINGYUN.Abp.Identity.Notifications.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.BackgroundTasks.Abstractions\LINGYUN.Abp.BackgroundTasks.Abstractions.csproj" />
</ItemGroup>
</Project>

29
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<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpIdentityJobsModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<IdentityResource>()
.AddVirtualJson("/LINGYUN/Abp/Identity/Jobs/Localization/Resources");
});
}
}

21
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"))
);
}
}

49
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;
/// <summary>
/// 用户会话清理作业
/// </summary>
/// <remarks>
/// 此作业启用时,建议禁用 <see cref="IdentitySessionCleanupOptions.IsCleanupEnabled"/>
/// </remarks>
public class InactiveIdentitySessionCleanupJob : IJobRunnable
{
public const string Name = "InactiveIdentitySessionCleanupJob";
#region Definition Paramters
public readonly static IReadOnlyList<JobDefinitionParamter> Paramters =
new List<JobDefinitionParamter>
{
new JobDefinitionParamter(
PropertySessionInactiveDays,
LocalizableStatic.Create("DisplayName:SessionInactiveDays"),
LocalizableStatic.Create("Description:SessionInactiveDays"))
};
#endregion
/// <summary>
/// 不活跃会话保持时长, 单位天
/// </summary>
public const string PropertySessionInactiveDays = "SessionInactiveDays";
public async virtual Task ExecuteAsync(JobRunnableContext context)
{
var logger = context.GetRequiredService<ILogger<InactiveIdentitySessionCleanupJob>>();
var sessionStore = context.GetRequiredService<IIdentitySessionStore>();
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.");
}
}

125
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;
///// <summary>
///// 用户清理作业
///// </summary>
///// <remarks>
///// 清理长期未登录用户
///// </remarks>
//public class InactiveIdentityUserCleanupJob : IJobRunnable
//{
// public const string Name = "InactiveIdentityUserCleanupJob";
// #region Definition Paramters
// public readonly static IReadOnlyList<JobDefinitionParamter> Paramters =
// new List<JobDefinitionParamter>
// {
// new JobDefinitionParamter(
// PropertyUserInactiveCleanupDays,
// LocalizableStatic.Create("DisplayName:UserInactiveCleanupDays"),
// LocalizableStatic.Create("Description:UserInactiveCleanupDays")),
// new JobDefinitionParamter(
// PropertyUserInactiveNotifierDays,
// LocalizableStatic.Create("DisplayName:UserInactiveNotifierDays"),
// LocalizableStatic.Create("Description:UserInactiveNotifierDays")),
// };
// #endregion
// /// <summary>
// /// 不活跃用户清理时间, 单位: 天
// /// </summary>
// public const string PropertyUserInactiveCleanupDays = "UserInactiveCleanupDays";
// /// <summary>
// /// 不活跃用户通知时间, 单位: 天
// /// </summary>
// public const string PropertyUserInactiveNotifierDays = "UserInactiveNotifierDays";
// public async virtual Task ExecuteAsync(JobRunnableContext context)
// {
// var logger = context.GetRequiredService<ILogger<InactiveIdentityUserCleanupJob>>();
// // 不活跃用户清理时间
// var inactiveCleanupDays = context.GetOrDefaultJobData(PropertyUserInactiveCleanupDays, 30);
// // 不活跃用户通知时间
// var inactiveNotifierDays = context.GetOrDefaultJobData(PropertyUserInactiveNotifierDays, 180);
// var clock = context.GetRequiredService<IClock>();
// var identityUserRepo = context.GetRequiredService<IIdentityUserRepository>();
// var identityUserSessionRepo = context.GetRequiredService<IIdentitySessionRepository>();
// // 获取需要清理的用户集合
// var specification = new ExpressionSpecification<IdentitySession>(
// 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.");
// }
// /// <summary>
// /// 发送不活跃用户清理通知
// /// </summary>
// /// <param name="context"></param>
// /// <param name="userSessions"></param>
// /// <returns></returns>
// private async Task SendInactiveUserNotifier(JobRunnableContext context, IEnumerable<IdentitySession> userSessions)
// {
// // TODO: 完成不活跃用户清理通知
// var notificationSender = context.GetRequiredService<INotificationSender>();
// var notificationTemplate = new NotificationTemplate(
// IdentityNotificationNames.IdentityUser.CleaningUpInactiveUsers,
// data: new Dictionary<string, object>
// {
// });
// await notificationSender.SendNofiterAsync(
// IdentityNotificationNames.IdentityUser.CleaningUpInactiveUsers,
// notificationTemplate
// );
// }
//}

12
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<IdentityResource>(name);
}
}

9
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."
}
}

9
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": "不活跃会话保持时长,单位: 天."
}
}

164
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;
/// <summary>
/// 会话控制事件处理器
/// </summary>
public class IdentitySessionAccessEventHandler :
IDistributedEventHandler<IdentitySessionChangeAccessedEvent>,
IDistributedEventHandler<EntityCreatedEto<IdentitySessionEto>>,
IDistributedEventHandler<EntityDeletedEto<UserEto>>,
ITransientDependency
{
public ILogger<IdentitySessionAccessEventHandler> 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<IdentitySessionAccessEventHandler>.Instance;
}
[UnitOfWork]
public async virtual Task HandleEventAsync(EntityCreatedEto<IdentitySessionEto> eventData)
{
// 新会话创建时检查登录策略
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto<IdentitySessionEto>)}";
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<UserEto> eventData)
{
// 用户被删除, 移除所有会话
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto<UserEto>)}";
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<ConcurrentLoginStrategy>(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;
}
}
}
}

164
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;
/// <summary>
/// 会话控制事件处理器
/// </summary>
public class IdentitySessionAccessEventHandler :
IDistributedEventHandler<IdentitySessionChangeAccessedEvent>,
IDistributedEventHandler<EntityCreatedEto<IdentitySessionEto>>,
IDistributedEventHandler<EntityDeletedEto<UserEto>>,
ITransientDependency
{
public ILogger<IdentitySessionAccessEventHandler> 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<IdentitySessionAccessEventHandler>.Instance;
}
[UnitOfWork]
public async virtual Task HandleEventAsync(EntityCreatedEto<IdentitySessionEto> eventData)
{
// 新会话创建时检查登录策略
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto<IdentitySessionEto>)}";
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<UserEto> eventData)
{
// 用户被删除, 移除所有会话
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto<UserEto>)}";
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<ConcurrentLoginStrategy>(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;
}
}
}
}

7
aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/EventBus/Local/UserSubscribeSessionExpirationEventHandler.cs

@ -19,10 +19,17 @@ public class UserSubscribeSessionExpirationEventHandler : ILocalEventHandler<Ent
public async virtual Task HandleEventAsync(EntityCreatedEventData<UserEto> 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);
}
}

Loading…
Cancel
Save