From 3ba69664dce0ac7390245e17bd6e0992345b24ab Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 13 Aug 2026 09:39:10 +0800 Subject: [PATCH] feat: User password change cancellation session --- .../IdentitySessionAccessEventHandler.cs | 42 +++- .../IdentityUserChangedEventHandler.cs | 28 +++ .../LINGYUN/Abp/Account/AccountAppService.cs | 22 +- .../Abp/Identity/IdentitySessionEto.cs | 13 +- .../IdentityUserSessionPasswordChangedEto.cs | 10 + .../Abp/Identity/AbpIdentityUserManager.cs | 110 ++++++++++ .../Abp/Identity/IdentityDomainMappers.cs | 18 ++ .../Session/IIdentitySessionManager.cs | 6 +- .../Session/IdentitySessionManager.cs | 5 +- .../IdentitySessionAccessEventHandler.cs | 190 ++++++++++++++++++ .../IdentityUserChangedEventHandler.cs | 27 +++ .../IdentitySessionAccessEventHandler.cs | 42 +++- .../IdentityUserChangedEventHandler.cs | 28 +++ 13 files changed, 508 insertions(+), 33 deletions(-) create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentityUserChangedEventHandler.cs create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityUserSessionPasswordChangedEto.cs create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/AbpIdentityUserManager.cs create mode 100644 aspnet-core/services/LY.MicroService.Applications.Single/EventBus/Distributed/IdentitySessionAccessEventHandler.cs create mode 100644 aspnet-core/services/LY.MicroService.Applications.Single/EventBus/Distributed/IdentityUserChangedEventHandler.cs create mode 100644 aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentityUserChangedEventHandler.cs diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentitySessionAccessEventHandler.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentitySessionAccessEventHandler.cs index fe3fb7f87..8bf28e440 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentitySessionAccessEventHandler.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentitySessionAccessEventHandler.cs @@ -18,6 +18,7 @@ namespace LINGYUN.Abp.MicroService.IdentityService.Handlers; /// 会话控制事件处理器 /// public class IdentitySessionAccessEventHandler : + IDistributedEventHandler, IDistributedEventHandler, IDistributedEventHandler>, IDistributedEventHandler>, @@ -50,11 +51,11 @@ public class IdentitySessionAccessEventHandler : var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto)}"; await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) { - Logger.LogInformation($"Lock is acquired for {lockKey}"); + Logger.LogDebug($"Lock is acquired for {lockKey}"); if (handle == null) { - Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); return; } @@ -69,18 +70,43 @@ public class IdentitySessionAccessEventHandler : var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto)}"; await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) { - Logger.LogInformation($"Lock is acquired for {lockKey}"); + Logger.LogDebug($"Lock is acquired for {lockKey}"); if (handle == null) { - Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); return; } + Logger.LogDebug("Due to the deletion of user {Id}, all sessions have been cancelled.", eventData.Entity.Id); await IdentitySessionStore.RevokeAllAsync(eventData.Entity.Id); } } + [UnitOfWork] + public async virtual Task HandleEventAsync(IdentityUserSessionPasswordChangedEto eventData) + { + if (!eventData.SessionId.IsNullOrWhiteSpace() && + Guid.TryParse(eventData.SessionId, out var exceptSessionId)) + { + // 用户密码更新使会话过期 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentityUserSessionPasswordChangedEto)}"; + await using var handle = await DistributedLock.TryAcquireAsync(lockKey); + + Logger.LogDebug($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); + return; + } + + Logger.LogDebug("Due to the password update of user {Id}, all sessions have been revoked.", eventData.Id); + + await IdentitySessionStore.RevokeAllAsync(eventData.Id, exceptSessionId); + } + } + [UnitOfWork] public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData) { @@ -88,11 +114,11 @@ public class IdentitySessionAccessEventHandler : var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentitySessionChangeAccessedEvent)}"; await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) { - Logger.LogInformation($"Lock is acquired for {lockKey}"); + Logger.LogDebug($"Lock is acquired for {lockKey}"); if (handle == null) { - Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); return; } @@ -106,11 +132,15 @@ public class IdentitySessionAccessEventHandler : idetitySession.UpdateLastAccessedTime(eventData.LastAccessed); await IdentitySessionStore.UpdateAsync(idetitySession); + + Logger.LogDebug("User session {SessionId} has been updated.", eventData.SessionId); } else { // 数据库中不存在会话, 清理缓存, 后续请求会话失效 await IdentitySessionCache.RemoveAsync(eventData.SessionId); + + Logger.LogWarning("User session {SessionId} is invalid. Remove all session caches.", eventData.SessionId); } } } diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentityUserChangedEventHandler.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentityUserChangedEventHandler.cs new file mode 100644 index 000000000..4009460f7 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentityUserChangedEventHandler.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Identity; +using Volo.Abp.Uow; +using Volo.Abp.Users; + +namespace LINGYUN.Abp.MicroService.IdentityService.Handlers; + +public class IdentityUserChangedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + protected IdentityUserManager UserManager { get; } + + public IdentityUserChangedEventHandler(IdentityUserManager userManager) + { + UserManager = userManager; + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(EntityCreatedEto eventData) + { + var user = await UserManager.GetByIdAsync(eventData.Entity.Id); + await UserManager.AddDefaultRolesAsync(user); + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/AccountAppService.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/AccountAppService.cs index 6c014341f..ca4624aff 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/AccountAppService.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/AccountAppService.cs @@ -69,7 +69,7 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi { userName = "wxid-" + wehchatOpenId.OpenId.ToMd5().ToLower(); } - + var userEmail = input.EmailAddress;//如果邮件地址不验证,随意写入一个 if (userEmail.IsNullOrWhiteSpace()) { @@ -102,7 +102,7 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi await CheckNewUserPhoneNumberNotBeUsedAsync(input.PhoneNumber); var securityTokenCacheKey = SecurityTokenCacheItem.CalculateSmsCacheKey( - input.PhoneNumber, + input.PhoneNumber, UserTwoFactorTokenProviderConsts.PhoneNumberRegisterPurpose); var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); var interval = await SettingProvider.GetAsync(IdentitySettingNames.User.SmsRepetInterval, 1); @@ -124,8 +124,8 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi await UserStore.SetSecurityStampAsync(tempNewUser, Guid.NewGuid().ToString("n")); var code = await UserManager.GenerateUserTokenAsync( - tempNewUser, - UserTwoFactorTokenProviderConsts.PhoneNumberRegisterTokenProvider, + tempNewUser, + UserTwoFactorTokenProviderConsts.PhoneNumberRegisterTokenProvider, UserTwoFactorTokenProviderConsts.PhoneNumberRegisterPurpose); securityTokenCacheItem = new SecurityTokenCacheItem(code, tempNewUser.Id, await UserManager.GetSecurityStampAsync(tempNewUser)); @@ -169,8 +169,8 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi await UserStore.SetSecurityStampAsync(tempNewUser, securityTokenCacheItem.SecurityToken); if (await UserManager.VerifyUserTokenAsync( - tempNewUser, - UserTwoFactorTokenProviderConsts.PhoneNumberRegisterTokenProvider, + tempNewUser, + UserTwoFactorTokenProviderConsts.PhoneNumberRegisterTokenProvider, UserTwoFactorTokenProviderConsts.PhoneNumberRegisterPurpose, input.Code)) { @@ -180,11 +180,12 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi { Name = input.Name ?? input.PhoneNumber }; + (await UserManager.CreateAsync(user)).CheckErrors(); (await UserManager.SetPhoneNumberAsync(user, input.PhoneNumber)).CheckErrors(); if (!input.Password.IsNullOrWhiteSpace()) { - (await UserManager.CreateAsync(user, input.Password)).CheckErrors(); + (await UserManager.AddPasswordAsync(user, input.Password)).CheckErrors(); } await UserStore.SetPhoneNumberConfirmedAsync(user, true); @@ -235,7 +236,7 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsResetPassword); // 生成二次认证码 var code = await UserManager.GenerateUserTokenAsync( - user, + user, TokenOptions.DefaultPhoneProvider, UserTwoFactorTokenProviderConsts.PhoneResetPasswordPurpose); // 发送短信验证码 @@ -270,9 +271,9 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi } // 验证二次认证码 if (!await UserManager.VerifyUserTokenAsync( - user, + user, TokenOptions.DefaultPhoneProvider, - UserTwoFactorTokenProviderConsts.PhoneResetPasswordPurpose, + UserTwoFactorTokenProviderConsts.PhoneResetPasswordPurpose, input.Code)) { // 验证码无效 @@ -282,6 +283,7 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi var resetPwdToken = await UserManager.GeneratePasswordResetTokenAsync(user); // 重置密码 (await UserManager.ResetPasswordAsync(user, resetPwdToken, input.NewPassword)).CheckErrors(); + // 移除缓存项 await SecurityTokenCache.RemoveAsync(securityTokenCacheKey); diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentitySessionEto.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentitySessionEto.cs index a748b6ed9..1af919c54 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentitySessionEto.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentitySessionEto.cs @@ -1,10 +1,11 @@ using System; +using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.MultiTenancy; namespace LINGYUN.Abp.Identity; [Serializable] -public class IdentitySessionEto : IMultiTenant +public class IdentitySessionEto : EtoBase, IMultiTenant { public Guid Id { get; set; } @@ -30,14 +31,14 @@ public class IdentitySessionEto : IMultiTenant } public IdentitySessionEto( - Guid id, + Guid id, string sessionId, - string device, + string device, string deviceInfo, - Guid userId, + Guid userId, string clientId, - string ipAddresses, - DateTime signedIn, + string ipAddresses, + DateTime signedIn, DateTime? lastAccessed, Guid? tenantId = null) { diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityUserSessionPasswordChangedEto.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityUserSessionPasswordChangedEto.cs new file mode 100644 index 000000000..3443af2c8 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityUserSessionPasswordChangedEto.cs @@ -0,0 +1,10 @@ +using System; +using Volo.Abp.Identity; + +namespace LINGYUN.Abp.Identity; + +[Serializable] +public class IdentityUserSessionPasswordChangedEto : IdentityUserPasswordChangedEto +{ + public string SessionId { get; set; } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/AbpIdentityUserManager.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/AbpIdentityUserManager.cs new file mode 100644 index 000000000..f32427ebd --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/AbpIdentityUserManager.cs @@ -0,0 +1,110 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Caching; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Security.Claims; +using Volo.Abp.Settings; +using Volo.Abp.Threading; +using Volo.Abp.Users; + +namespace LINGYUN.Abp.Identity; + +[Dependency(ReplaceServices = true)] +[ExposeServices( + typeof(IdentityUserManager), + typeof(AbpIdentityUserManager), + typeof(UserManager))] +public class AbpIdentityUserManager : IdentityUserManager +{ + public AbpIdentityUserManager( + IdentityUserStore store, + Volo.Abp.Identity.IIdentityRoleRepository roleRepository, + Volo.Abp.Identity.IIdentityUserRepository userRepository, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger logger, + ICancellationTokenProvider cancellationTokenProvider, + Volo.Abp.Identity.IOrganizationUnitRepository organizationUnitRepository, + ISettingProvider settingProvider, + IDistributedEventBus distributedEventBus, + IIdentityLinkUserRepository identityLinkUserRepository, + IDistributedCache dynamicClaimCache, + IOptions multiTenancyOptions, + ICurrentTenant currentTenant, + IDataFilter dataFilter) + : base( + store, + roleRepository, + userRepository, + optionsAccessor, + passwordHasher, + userValidators, + passwordValidators, + keyNormalizer, + errors, + services, + logger, + cancellationTokenProvider, + organizationUnitRepository, + settingProvider, + distributedEventBus, + identityLinkUserRepository, + dynamicClaimCache, + multiTenancyOptions, + currentTenant, + dataFilter) + { + } + + public async override Task ResetPasswordAsync(IdentityUser user, string token, string newPassword) + { + var result = await base.ResetPasswordAsync(user, token, newPassword); + + result.CheckErrors(); + + var currentUser = ServiceProvider.GetService(); + + await DistributedEventBus.PublishAsync(new IdentityUserSessionPasswordChangedEto + { + Id = user.Id, + TenantId = user.TenantId, + Email = user.Email, + SessionId = currentUser?.FindSessionId(), + }); + + return result; + } + + public async override Task ChangePasswordAsync(IdentityUser user, string currentPassword, string newPassword) + { + var result = await base.ChangePasswordAsync(user, currentPassword, newPassword); + + result.CheckErrors(); + + var currentUser = ServiceProvider.GetService(); + + await DistributedEventBus.PublishAsync(new IdentityUserSessionPasswordChangedEto + { + Id = user.Id, + TenantId = user.TenantId, + Email = user.Email, + SessionId = currentUser?.FindSessionId(), + }); + + return result; + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IdentityDomainMappers.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IdentityDomainMappers.cs index df59fd38d..049bdc239 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IdentityDomainMappers.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IdentityDomainMappers.cs @@ -1,4 +1,5 @@ using Riok.Mapperly.Abstractions; +using System.Collections.Generic; using Volo.Abp.Identity; using Volo.Abp.Mapperly; @@ -7,6 +8,23 @@ namespace LINGYUN.Abp.Identity; [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] public partial class IdentitySessionToIdentitySessionEtoMapper : MapperBase { + [MapPropertyFromSource(nameof(IdentitySessionEto.Properties), Use = nameof(TryGetProperties))] public override partial IdentitySessionEto Map(IdentitySession source); + + [MapPropertyFromSource(nameof(IdentitySessionEto.Properties), Use = nameof(TryGetProperties))] public override partial void Map(IdentitySession source, IdentitySessionEto destination); + + [UserMapping(Default = false)] + private static Dictionary TryGetProperties(IdentitySession source) + { + var properties = new Dictionary(); + if (source != null && source.ExtraProperties != null) + { + foreach (var property in source.ExtraProperties) + { + properties[property.Key] = property.Value.ToString(); + } + } + return properties; + } } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionManager.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionManager.cs index faabf8d19..0229125b0 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionManager.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionManager.cs @@ -15,15 +15,15 @@ public interface IIdentitySessionManager /// /// Task SaveSessionAsync( - ClaimsPrincipal claimsPrincipal, + ClaimsPrincipal claimsPrincipal, CancellationToken cancellationToken = default); /// /// 撤销用户会话 /// /// 会话id - /// + /// /// Task RevokeSessionAsync( string sessionId, - CancellationToken cancellation = default); + CancellationToken cancellationToken = default); } 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 b06162497..9c2c1f0b6 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 @@ -10,6 +10,7 @@ using Volo.Abp.Identity; using Volo.Abp.Security.Claims; namespace LINGYUN.Abp.Identity.Session; + public class IdentitySessionManager : DomainService, IIdentitySessionManager { protected IDeviceInfoProvider DeviceInfoProvider { get; } @@ -99,9 +100,9 @@ public class IdentitySessionManager : DomainService, IIdentitySessionManager public async virtual Task RevokeSessionAsync( string sessionId, - CancellationToken cancellation = default) + CancellationToken cancellationToken = default) { Logger.LogDebug($"Revoke user session for: {sessionId}"); - await IdentitySessionStore.RevokeAsync(sessionId, cancellation); + await IdentitySessionStore.RevokeAsync(sessionId, cancellationToken: cancellationToken); } } diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/EventBus/Distributed/IdentitySessionAccessEventHandler.cs b/aspnet-core/services/LY.MicroService.Applications.Single/EventBus/Distributed/IdentitySessionAccessEventHandler.cs new file mode 100644 index 000000000..c8ccebd7d --- /dev/null +++ b/aspnet-core/services/LY.MicroService.Applications.Single/EventBus/Distributed/IdentitySessionAccessEventHandler.cs @@ -0,0 +1,190 @@ +using LINGYUN.Abp.Identity.Settings; +using Microsoft.Extensions.Logging.Abstractions; +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.Applications.Single.EventBus.Distributed; +/// +/// 会话控制事件处理器 +/// +public class IdentitySessionAccessEventHandler : + IDistributedEventHandler, + 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.LogDebug($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogDebug($"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.LogDebug($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); + return; + } + Logger.LogDebug("Due to the deletion of user {Id}, all sessions have been cancelled.", eventData.Entity.Id); + + await IdentitySessionStore.RevokeAllAsync(eventData.Entity.Id); + } + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(IdentityUserSessionPasswordChangedEto eventData) + { + if (!eventData.SessionId.IsNullOrWhiteSpace() && + Guid.TryParse(eventData.SessionId, out var exceptSessionId)) + { + // 用户密码更新使会话过期 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentityUserSessionPasswordChangedEto)}"; + await using var handle = await DistributedLock.TryAcquireAsync(lockKey); + + Logger.LogDebug($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); + return; + } + + Logger.LogDebug("Due to the password update of user {Id}, all sessions have been revoked.", eventData.Id); + + await IdentitySessionStore.RevokeAllAsync(eventData.Id, exceptSessionId); + } + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData) + { + // 会话访问更新 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentitySessionChangeAccessedEvent)}"; + await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) + { + Logger.LogDebug($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogDebug($"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); + + Logger.LogDebug("User session {SessionId} has been updated.", eventData.SessionId); + } + else + { + // 数据库中不存在会话, 清理缓存, 后续请求会话失效 + await IdentitySessionCache.RemoveAsync(eventData.SessionId); + + Logger.LogWarning("User session {SessionId} is invalid. Remove all session caches.", 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.Applications.Single/EventBus/Distributed/IdentityUserChangedEventHandler.cs b/aspnet-core/services/LY.MicroService.Applications.Single/EventBus/Distributed/IdentityUserChangedEventHandler.cs new file mode 100644 index 000000000..68bbd367c --- /dev/null +++ b/aspnet-core/services/LY.MicroService.Applications.Single/EventBus/Distributed/IdentityUserChangedEventHandler.cs @@ -0,0 +1,27 @@ +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Identity; +using Volo.Abp.Uow; +using Volo.Abp.Users; + +namespace LY.MicroService.Applications.Single.EventBus.Distributed; + +public class IdentityUserChangedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + protected IdentityUserManager UserManager { get; } + + public IdentityUserChangedEventHandler(IdentityUserManager userManager) + { + UserManager = userManager; + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(EntityCreatedEto eventData) + { + var user = await UserManager.GetByIdAsync(eventData.Entity.Id); + await UserManager.AddDefaultRolesAsync(user); + } +} 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 index 9de763da8..98095c257 100644 --- a/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs +++ b/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs @@ -18,6 +18,7 @@ namespace LY.MicroService.AuthServer.Handlers; /// 会话控制事件处理器 /// public class IdentitySessionAccessEventHandler : + IDistributedEventHandler, IDistributedEventHandler, IDistributedEventHandler>, IDistributedEventHandler>, @@ -50,11 +51,11 @@ public class IdentitySessionAccessEventHandler : var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto)}"; await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) { - Logger.LogInformation($"Lock is acquired for {lockKey}"); + Logger.LogDebug($"Lock is acquired for {lockKey}"); if (handle == null) { - Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); return; } @@ -69,18 +70,43 @@ public class IdentitySessionAccessEventHandler : var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto)}"; await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) { - Logger.LogInformation($"Lock is acquired for {lockKey}"); + Logger.LogDebug($"Lock is acquired for {lockKey}"); if (handle == null) { - Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); return; } + Logger.LogDebug("Due to the deletion of user {Id}, all sessions have been cancelled.", eventData.Entity.Id); await IdentitySessionStore.RevokeAllAsync(eventData.Entity.Id); } } + [UnitOfWork] + public async virtual Task HandleEventAsync(IdentityUserSessionPasswordChangedEto eventData) + { + if (!eventData.SessionId.IsNullOrWhiteSpace() && + Guid.TryParse(eventData.SessionId, out var exceptSessionId)) + { + // 用户密码更新使会话过期 + var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentityUserSessionPasswordChangedEto)}"; + await using var handle = await DistributedLock.TryAcquireAsync(lockKey); + + Logger.LogDebug($"Lock is acquired for {lockKey}"); + + if (handle == null) + { + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); + return; + } + + Logger.LogDebug("Due to the password update of user {Id}, all sessions have been revoked.", eventData.Id); + + await IdentitySessionStore.RevokeAllAsync(eventData.Id, exceptSessionId); + } + } + [UnitOfWork] public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData) { @@ -88,11 +114,11 @@ public class IdentitySessionAccessEventHandler : var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentitySessionChangeAccessedEvent)}"; await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) { - Logger.LogInformation($"Lock is acquired for {lockKey}"); + Logger.LogDebug($"Lock is acquired for {lockKey}"); if (handle == null) { - Logger.LogInformation($"Handle is null because of the locking for : {lockKey}"); + Logger.LogDebug($"Handle is null because of the locking for : {lockKey}"); return; } @@ -106,11 +132,15 @@ public class IdentitySessionAccessEventHandler : idetitySession.UpdateLastAccessedTime(eventData.LastAccessed); await IdentitySessionStore.UpdateAsync(idetitySession); + + Logger.LogDebug("User session {SessionId} has been updated.", eventData.SessionId); } else { // 数据库中不存在会话, 清理缓存, 后续请求会话失效 await IdentitySessionCache.RemoveAsync(eventData.SessionId); + + Logger.LogWarning("User session {SessionId} is invalid. Remove all session caches.", eventData.SessionId); } } } diff --git a/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentityUserChangedEventHandler.cs b/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentityUserChangedEventHandler.cs new file mode 100644 index 000000000..73e18c5f5 --- /dev/null +++ b/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentityUserChangedEventHandler.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Identity; +using Volo.Abp.Uow; +using Volo.Abp.Users; + +namespace LY.MicroService.AuthServer.Handlers; + +public class IdentityUserChangedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + protected IdentityUserManager UserManager { get; } + + public IdentityUserChangedEventHandler(IdentityUserManager userManager) + { + UserManager = userManager; + } + + [UnitOfWork] + public async virtual Task HandleEventAsync(EntityCreatedEto eventData) + { + var user = await UserManager.GetByIdAsync(eventData.Entity.Id); + await UserManager.AddDefaultRolesAsync(user); + } +}