Browse Source

Merge pull request #1543 from colinin/password-change-revoke-session

feat: User password change cancellation session
dev
yx lin 1 day ago
committed by GitHub
parent
commit
692dc0fa17
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 42
      aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentitySessionAccessEventHandler.cs
  2. 28
      aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentityUserChangedEventHandler.cs
  3. 22
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/AccountAppService.cs
  4. 13
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentitySessionEto.cs
  5. 10
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityUserSessionPasswordChangedEto.cs
  6. 110
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/AbpIdentityUserManager.cs
  7. 18
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IdentityDomainMappers.cs
  8. 6
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionManager.cs
  9. 5
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IdentitySessionManager.cs
  10. 190
      aspnet-core/services/LY.MicroService.Applications.Single/EventBus/Distributed/IdentitySessionAccessEventHandler.cs
  11. 27
      aspnet-core/services/LY.MicroService.Applications.Single/EventBus/Distributed/IdentityUserChangedEventHandler.cs
  12. 42
      aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs
  13. 28
      aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentityUserChangedEventHandler.cs

42
aspnet-core/aspire/LINGYUN.Abp.MicroService.IdentityService/Handlers/IdentitySessionAccessEventHandler.cs

@ -18,6 +18,7 @@ namespace LINGYUN.Abp.MicroService.IdentityService.Handlers;
/// 会话控制事件处理器 /// 会话控制事件处理器
/// </summary> /// </summary>
public class IdentitySessionAccessEventHandler : public class IdentitySessionAccessEventHandler :
IDistributedEventHandler<IdentityUserSessionPasswordChangedEto>,
IDistributedEventHandler<IdentitySessionChangeAccessedEvent>, IDistributedEventHandler<IdentitySessionChangeAccessedEvent>,
IDistributedEventHandler<EntityCreatedEto<IdentitySessionEto>>, IDistributedEventHandler<EntityCreatedEto<IdentitySessionEto>>,
IDistributedEventHandler<EntityDeletedEto<UserEto>>, IDistributedEventHandler<EntityDeletedEto<UserEto>>,
@ -50,11 +51,11 @@ public class IdentitySessionAccessEventHandler :
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto<IdentitySessionEto>)}"; var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto<IdentitySessionEto>)}";
await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) 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) 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; return;
} }
@ -69,18 +70,43 @@ public class IdentitySessionAccessEventHandler :
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto<UserEto>)}"; var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto<UserEto>)}";
await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) 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) 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; return;
} }
Logger.LogDebug("Due to the deletion of user {Id}, all sessions have been cancelled.", eventData.Entity.Id);
await IdentitySessionStore.RevokeAllAsync(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] [UnitOfWork]
public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData) public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData)
{ {
@ -88,11 +114,11 @@ public class IdentitySessionAccessEventHandler :
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentitySessionChangeAccessedEvent)}"; var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentitySessionChangeAccessedEvent)}";
await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) 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) 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; return;
} }
@ -106,11 +132,15 @@ public class IdentitySessionAccessEventHandler :
idetitySession.UpdateLastAccessedTime(eventData.LastAccessed); idetitySession.UpdateLastAccessedTime(eventData.LastAccessed);
await IdentitySessionStore.UpdateAsync(idetitySession); await IdentitySessionStore.UpdateAsync(idetitySession);
Logger.LogDebug("User session {SessionId} has been updated.", eventData.SessionId);
} }
else else
{ {
// 数据库中不存在会话, 清理缓存, 后续请求会话失效 // 数据库中不存在会话, 清理缓存, 后续请求会话失效
await IdentitySessionCache.RemoveAsync(eventData.SessionId); await IdentitySessionCache.RemoveAsync(eventData.SessionId);
Logger.LogWarning("User session {SessionId} is invalid. Remove all session caches.", eventData.SessionId);
} }
} }
} }

28
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<EntityCreatedEto<UserEto>>,
ITransientDependency
{
protected IdentityUserManager UserManager { get; }
public IdentityUserChangedEventHandler(IdentityUserManager userManager)
{
UserManager = userManager;
}
[UnitOfWork]
public async virtual Task HandleEventAsync(EntityCreatedEto<UserEto> eventData)
{
var user = await UserManager.GetByIdAsync(eventData.Entity.Id);
await UserManager.AddDefaultRolesAsync(user);
}
}

22
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(); userName = "wxid-" + wehchatOpenId.OpenId.ToMd5().ToLower();
} }
var userEmail = input.EmailAddress;//如果邮件地址不验证,随意写入一个 var userEmail = input.EmailAddress;//如果邮件地址不验证,随意写入一个
if (userEmail.IsNullOrWhiteSpace()) if (userEmail.IsNullOrWhiteSpace())
{ {
@ -102,7 +102,7 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi
await CheckNewUserPhoneNumberNotBeUsedAsync(input.PhoneNumber); await CheckNewUserPhoneNumberNotBeUsedAsync(input.PhoneNumber);
var securityTokenCacheKey = SecurityTokenCacheItem.CalculateSmsCacheKey( var securityTokenCacheKey = SecurityTokenCacheItem.CalculateSmsCacheKey(
input.PhoneNumber, input.PhoneNumber,
UserTwoFactorTokenProviderConsts.PhoneNumberRegisterPurpose); UserTwoFactorTokenProviderConsts.PhoneNumberRegisterPurpose);
var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey);
var interval = await SettingProvider.GetAsync(IdentitySettingNames.User.SmsRepetInterval, 1); 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")); await UserStore.SetSecurityStampAsync(tempNewUser, Guid.NewGuid().ToString("n"));
var code = await UserManager.GenerateUserTokenAsync( var code = await UserManager.GenerateUserTokenAsync(
tempNewUser, tempNewUser,
UserTwoFactorTokenProviderConsts.PhoneNumberRegisterTokenProvider, UserTwoFactorTokenProviderConsts.PhoneNumberRegisterTokenProvider,
UserTwoFactorTokenProviderConsts.PhoneNumberRegisterPurpose); UserTwoFactorTokenProviderConsts.PhoneNumberRegisterPurpose);
securityTokenCacheItem = new SecurityTokenCacheItem(code, tempNewUser.Id, await UserManager.GetSecurityStampAsync(tempNewUser)); 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); await UserStore.SetSecurityStampAsync(tempNewUser, securityTokenCacheItem.SecurityToken);
if (await UserManager.VerifyUserTokenAsync( if (await UserManager.VerifyUserTokenAsync(
tempNewUser, tempNewUser,
UserTwoFactorTokenProviderConsts.PhoneNumberRegisterTokenProvider, UserTwoFactorTokenProviderConsts.PhoneNumberRegisterTokenProvider,
UserTwoFactorTokenProviderConsts.PhoneNumberRegisterPurpose, UserTwoFactorTokenProviderConsts.PhoneNumberRegisterPurpose,
input.Code)) input.Code))
{ {
@ -180,11 +180,12 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi
{ {
Name = input.Name ?? input.PhoneNumber Name = input.Name ?? input.PhoneNumber
}; };
(await UserManager.CreateAsync(user)).CheckErrors();
(await UserManager.SetPhoneNumberAsync(user, input.PhoneNumber)).CheckErrors(); (await UserManager.SetPhoneNumberAsync(user, input.PhoneNumber)).CheckErrors();
if (!input.Password.IsNullOrWhiteSpace()) if (!input.Password.IsNullOrWhiteSpace())
{ {
(await UserManager.CreateAsync(user, input.Password)).CheckErrors(); (await UserManager.AddPasswordAsync(user, input.Password)).CheckErrors();
} }
await UserStore.SetPhoneNumberConfirmedAsync(user, true); await UserStore.SetPhoneNumberConfirmedAsync(user, true);
@ -235,7 +236,7 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi
var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsResetPassword); var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsResetPassword);
// 生成二次认证码 // 生成二次认证码
var code = await UserManager.GenerateUserTokenAsync( var code = await UserManager.GenerateUserTokenAsync(
user, user,
TokenOptions.DefaultPhoneProvider, TokenOptions.DefaultPhoneProvider,
UserTwoFactorTokenProviderConsts.PhoneResetPasswordPurpose); UserTwoFactorTokenProviderConsts.PhoneResetPasswordPurpose);
// 发送短信验证码 // 发送短信验证码
@ -270,9 +271,9 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi
} }
// 验证二次认证码 // 验证二次认证码
if (!await UserManager.VerifyUserTokenAsync( if (!await UserManager.VerifyUserTokenAsync(
user, user,
TokenOptions.DefaultPhoneProvider, TokenOptions.DefaultPhoneProvider,
UserTwoFactorTokenProviderConsts.PhoneResetPasswordPurpose, UserTwoFactorTokenProviderConsts.PhoneResetPasswordPurpose,
input.Code)) input.Code))
{ {
// 验证码无效 // 验证码无效
@ -282,6 +283,7 @@ public class AccountAppService : AccountApplicationServiceBase, IAccountAppServi
var resetPwdToken = await UserManager.GeneratePasswordResetTokenAsync(user); var resetPwdToken = await UserManager.GeneratePasswordResetTokenAsync(user);
// 重置密码 // 重置密码
(await UserManager.ResetPasswordAsync(user, resetPwdToken, input.NewPassword)).CheckErrors(); (await UserManager.ResetPasswordAsync(user, resetPwdToken, input.NewPassword)).CheckErrors();
// 移除缓存项 // 移除缓存项
await SecurityTokenCache.RemoveAsync(securityTokenCacheKey); await SecurityTokenCache.RemoveAsync(securityTokenCacheKey);

13
aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentitySessionEto.cs

@ -1,10 +1,11 @@
using System; using System;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
namespace LINGYUN.Abp.Identity; namespace LINGYUN.Abp.Identity;
[Serializable] [Serializable]
public class IdentitySessionEto : IMultiTenant public class IdentitySessionEto : EtoBase, IMultiTenant
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
@ -30,14 +31,14 @@ public class IdentitySessionEto : IMultiTenant
} }
public IdentitySessionEto( public IdentitySessionEto(
Guid id, Guid id,
string sessionId, string sessionId,
string device, string device,
string deviceInfo, string deviceInfo,
Guid userId, Guid userId,
string clientId, string clientId,
string ipAddresses, string ipAddresses,
DateTime signedIn, DateTime signedIn,
DateTime? lastAccessed, DateTime? lastAccessed,
Guid? tenantId = null) Guid? tenantId = null)
{ {

10
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; }
}

110
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<IdentityUser>))]
public class AbpIdentityUserManager : IdentityUserManager
{
public AbpIdentityUserManager(
IdentityUserStore store,
Volo.Abp.Identity.IIdentityRoleRepository roleRepository,
Volo.Abp.Identity.IIdentityUserRepository userRepository,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher,
IEnumerable<IUserValidator<IdentityUser>> userValidators,
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<IdentityUserManager> logger,
ICancellationTokenProvider cancellationTokenProvider,
Volo.Abp.Identity.IOrganizationUnitRepository organizationUnitRepository,
ISettingProvider settingProvider,
IDistributedEventBus distributedEventBus,
IIdentityLinkUserRepository identityLinkUserRepository,
IDistributedCache<AbpDynamicClaimCacheItem> dynamicClaimCache,
IOptions<AbpMultiTenancyOptions> 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<IdentityResult> ResetPasswordAsync(IdentityUser user, string token, string newPassword)
{
var result = await base.ResetPasswordAsync(user, token, newPassword);
result.CheckErrors();
var currentUser = ServiceProvider.GetService<ICurrentUser>();
await DistributedEventBus.PublishAsync(new IdentityUserSessionPasswordChangedEto
{
Id = user.Id,
TenantId = user.TenantId,
Email = user.Email,
SessionId = currentUser?.FindSessionId(),
});
return result;
}
public async override Task<IdentityResult> ChangePasswordAsync(IdentityUser user, string currentPassword, string newPassword)
{
var result = await base.ChangePasswordAsync(user, currentPassword, newPassword);
result.CheckErrors();
var currentUser = ServiceProvider.GetService<ICurrentUser>();
await DistributedEventBus.PublishAsync(new IdentityUserSessionPasswordChangedEto
{
Id = user.Id,
TenantId = user.TenantId,
Email = user.Email,
SessionId = currentUser?.FindSessionId(),
});
return result;
}
}

18
aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IdentityDomainMappers.cs

@ -1,4 +1,5 @@
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
using System.Collections.Generic;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using Volo.Abp.Mapperly; using Volo.Abp.Mapperly;
@ -7,6 +8,23 @@ namespace LINGYUN.Abp.Identity;
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class IdentitySessionToIdentitySessionEtoMapper : MapperBase<IdentitySession, IdentitySessionEto> public partial class IdentitySessionToIdentitySessionEtoMapper : MapperBase<IdentitySession, IdentitySessionEto>
{ {
[MapPropertyFromSource(nameof(IdentitySessionEto.Properties), Use = nameof(TryGetProperties))]
public override partial IdentitySessionEto Map(IdentitySession source); public override partial IdentitySessionEto Map(IdentitySession source);
[MapPropertyFromSource(nameof(IdentitySessionEto.Properties), Use = nameof(TryGetProperties))]
public override partial void Map(IdentitySession source, IdentitySessionEto destination); public override partial void Map(IdentitySession source, IdentitySessionEto destination);
[UserMapping(Default = false)]
private static Dictionary<string, string> TryGetProperties(IdentitySession source)
{
var properties = new Dictionary<string, string>();
if (source != null && source.ExtraProperties != null)
{
foreach (var property in source.ExtraProperties)
{
properties[property.Key] = property.Value.ToString();
}
}
return properties;
}
} }

6
aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/Session/IIdentitySessionManager.cs

@ -15,15 +15,15 @@ public interface IIdentitySessionManager
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
Task SaveSessionAsync( Task SaveSessionAsync(
ClaimsPrincipal claimsPrincipal, ClaimsPrincipal claimsPrincipal,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 撤销用户会话 /// 撤销用户会话
/// </summary> /// </summary>
/// <param name="sessionId">会话id</param> /// <param name="sessionId">会话id</param>
/// <param name="cancellation"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
Task RevokeSessionAsync( Task RevokeSessionAsync(
string sessionId, string sessionId,
CancellationToken cancellation = default); CancellationToken cancellationToken = default);
} }

5
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; using Volo.Abp.Security.Claims;
namespace LINGYUN.Abp.Identity.Session; namespace LINGYUN.Abp.Identity.Session;
public class IdentitySessionManager : DomainService, IIdentitySessionManager public class IdentitySessionManager : DomainService, IIdentitySessionManager
{ {
protected IDeviceInfoProvider DeviceInfoProvider { get; } protected IDeviceInfoProvider DeviceInfoProvider { get; }
@ -99,9 +100,9 @@ public class IdentitySessionManager : DomainService, IIdentitySessionManager
public async virtual Task RevokeSessionAsync( public async virtual Task RevokeSessionAsync(
string sessionId, string sessionId,
CancellationToken cancellation = default) CancellationToken cancellationToken = default)
{ {
Logger.LogDebug($"Revoke user session for: {sessionId}"); Logger.LogDebug($"Revoke user session for: {sessionId}");
await IdentitySessionStore.RevokeAsync(sessionId, cancellation); await IdentitySessionStore.RevokeAsync(sessionId, cancellationToken: cancellationToken);
} }
} }

190
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;
/// <summary>
/// 会话控制事件处理器
/// </summary>
public class IdentitySessionAccessEventHandler :
IDistributedEventHandler<IdentityUserSessionPasswordChangedEto>,
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.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<UserEto> eventData)
{
// 用户被删除, 移除所有会话
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto<UserEto>)}";
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<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;
}
}
}
}

27
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<EntityCreatedEto<UserEto>>,
ITransientDependency
{
protected IdentityUserManager UserManager { get; }
public IdentityUserChangedEventHandler(IdentityUserManager userManager)
{
UserManager = userManager;
}
[UnitOfWork]
public async virtual Task HandleEventAsync(EntityCreatedEto<UserEto> eventData)
{
var user = await UserManager.GetByIdAsync(eventData.Entity.Id);
await UserManager.AddDefaultRolesAsync(user);
}
}

42
aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/Handlers/IdentitySessionAccessEventHandler.cs

@ -18,6 +18,7 @@ namespace LY.MicroService.AuthServer.Handlers;
/// 会话控制事件处理器 /// 会话控制事件处理器
/// </summary> /// </summary>
public class IdentitySessionAccessEventHandler : public class IdentitySessionAccessEventHandler :
IDistributedEventHandler<IdentityUserSessionPasswordChangedEto>,
IDistributedEventHandler<IdentitySessionChangeAccessedEvent>, IDistributedEventHandler<IdentitySessionChangeAccessedEvent>,
IDistributedEventHandler<EntityCreatedEto<IdentitySessionEto>>, IDistributedEventHandler<EntityCreatedEto<IdentitySessionEto>>,
IDistributedEventHandler<EntityDeletedEto<UserEto>>, IDistributedEventHandler<EntityDeletedEto<UserEto>>,
@ -50,11 +51,11 @@ public class IdentitySessionAccessEventHandler :
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto<IdentitySessionEto>)}"; var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityCreatedEto<IdentitySessionEto>)}";
await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) 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) 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; return;
} }
@ -69,18 +70,43 @@ public class IdentitySessionAccessEventHandler :
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto<UserEto>)}"; var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(EntityDeletedEto<UserEto>)}";
await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) 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) 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; return;
} }
Logger.LogDebug("Due to the deletion of user {Id}, all sessions have been cancelled.", eventData.Entity.Id);
await IdentitySessionStore.RevokeAllAsync(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] [UnitOfWork]
public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData) public async virtual Task HandleEventAsync(IdentitySessionChangeAccessedEvent eventData)
{ {
@ -88,11 +114,11 @@ public class IdentitySessionAccessEventHandler :
var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentitySessionChangeAccessedEvent)}"; var lockKey = $"{nameof(IdentitySessionAccessEventHandler)}_{nameof(IdentitySessionChangeAccessedEvent)}";
await using (var handle = await DistributedLock.TryAcquireAsync(lockKey)) 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) 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; return;
} }
@ -106,11 +132,15 @@ public class IdentitySessionAccessEventHandler :
idetitySession.UpdateLastAccessedTime(eventData.LastAccessed); idetitySession.UpdateLastAccessedTime(eventData.LastAccessed);
await IdentitySessionStore.UpdateAsync(idetitySession); await IdentitySessionStore.UpdateAsync(idetitySession);
Logger.LogDebug("User session {SessionId} has been updated.", eventData.SessionId);
} }
else else
{ {
// 数据库中不存在会话, 清理缓存, 后续请求会话失效 // 数据库中不存在会话, 清理缓存, 后续请求会话失效
await IdentitySessionCache.RemoveAsync(eventData.SessionId); await IdentitySessionCache.RemoveAsync(eventData.SessionId);
Logger.LogWarning("User session {SessionId} is invalid. Remove all session caches.", eventData.SessionId);
} }
} }
} }

28
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<EntityCreatedEto<UserEto>>,
ITransientDependency
{
protected IdentityUserManager UserManager { get; }
public IdentityUserChangedEventHandler(IdentityUserManager userManager)
{
UserManager = userManager;
}
[UnitOfWork]
public async virtual Task HandleEventAsync(EntityCreatedEto<UserEto> eventData)
{
var user = await UserManager.GetByIdAsync(eventData.Entity.Id);
await UserManager.AddDefaultRolesAsync(user);
}
}
Loading…
Cancel
Save