From 4579fc3a6ddbf5598b81eff92947e73b706da0e9 Mon Sep 17 00:00:00 2001 From: colin Date: Tue, 16 Jun 2026 11:11:13 +0800 Subject: [PATCH] feat: Implement password reuse check --- .../LINGYUN/Abp/Account/ProfileAppService.cs | 65 +++++++++++++++++++ .../Abp/Identity/IdentityErrorCodes.cs | 4 ++ .../LINGYUN/Abp/Identity/Localization/en.json | 1 + .../Abp/Identity/Localization/zh-Hans.json | 1 + .../Abp/Identity/AbpIdentityDomainModule.cs | 3 +- .../PasswordHistoryPasswordValidator.cs | 33 ++++++++++ 6 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/ProfileAppService.cs create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/PasswordHistoryPasswordValidator.cs diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/ProfileAppService.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/ProfileAppService.cs new file mode 100644 index 000000000..e78e0013a --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/ProfileAppService.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Account; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; +using Volo.Abp.Identity.Settings; +using Volo.Abp.Settings; +using Volo.Abp.Users; + +namespace LINGYUN.Abp.Account; + +[Dependency(ReplaceServices = true)] +public class ProfileAppService : Volo.Abp.Account.ProfileAppService +{ + public ProfileAppService( + IdentityUserManager userManager, + IOptions identityOptions) + : base(userManager, identityOptions) + { + } + + public async override Task ChangePasswordAsync(ChangePasswordInput input) + { + await IdentityOptions.SetAsync(); + + var currentUser = await UserManager.GetByIdAsync(CurrentUser.GetId()); + + if (currentUser.IsExternal) + { + throw new BusinessException(code: IdentityErrorCodes.ExternalUserPasswordChange); + } + + if (currentUser.PasswordHash.IsNullOrWhiteSpace()) + { + (await UserManager.AddPasswordAsync(currentUser, input.NewPassword)).CheckErrors(); + } + else + { + (await UserManager.ChangePasswordAsync(currentUser, input.CurrentPassword, input.NewPassword)).CheckErrors(); + } + + if (await SettingProvider.IsTrueAsync(IdentitySettingNames.Password.EnablePreventPasswordReuse)) + { + var preventPasswordReuseCount = await SettingProvider.GetAsync(IdentitySettingNames.Password.PreventPasswordReuseCount, 6); + currentUser.AddPasswordHistory(input.NewPassword); + if (currentUser.PasswordHistories.Count > preventPasswordReuseCount) + { + var excessCount = currentUser.PasswordHistories.Count - preventPasswordReuseCount; + var oldestHistories = currentUser.PasswordHistories + .OrderBy(x => x.CreatedAt) + .Take(excessCount) + .ToList(); + + foreach (var history in oldestHistories) + { + currentUser.PasswordHistories.Remove(history); + } + } + } + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityErrorCodes.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityErrorCodes.cs index ab60c20ba..3b1f90c3b 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityErrorCodes.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityErrorCodes.cs @@ -34,4 +34,8 @@ public class IdentityErrorCodes /// 验证器验证无效 /// public const string AuthenticatorTokenInValid = "Volo.Abp.Identity:020012"; + /// + /// 密码不能与最近{0}次使用的密码相同 + /// + public const string PasswordInHistoryInValid = "Volo.Abp.Identity:020013"; } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json index 5ed0b7b6c..82450d78d 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json @@ -53,6 +53,7 @@ "Volo.Abp.Identity:020010": "The email address is bound!", "Volo.Abp.Identity:020011": "Secondary authentication cannot be enabled when an MFA device is not attached!", "Volo.Abp.Identity:020012": "Invalid authenticator code!", + "Volo.Abp.Identity:020013": "The password cannot be the same as the previous one.!", "Volo.Abp.Identity:DuplicatePhoneNumber": "Phone number '{0}' is already taken.", "DisplayName:Abp.Identity.User.SmsNewUserRegister": "Register sms template", "Description:Abp.Identity.User.SmsNewUserRegister": "When the user registers, he/she should send the template number of the SMS verification code and fill in the template number of the corresponding cloud platform registration", diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json index f2b073d39..4187cf83c 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json @@ -53,6 +53,7 @@ "Volo.Abp.Identity:020010": "邮件地址已绑定!", "Volo.Abp.Identity:020011": "未绑定MFA设备时无法启用二次认证!", "Volo.Abp.Identity:020012": "无效的验证器代码!", + "Volo.Abp.Identity:020013": "密码不能与之前的密码相同!", "Volo.Abp.Identity:DuplicatePhoneNumber": "手机号 '{0}' 已存在.", "DisplayName:Abp.Identity.User.SmsNewUserRegister": "新用户注册模板", "Description:Abp.Identity.User.SmsNewUserRegister": "新用户通过手机注册账号验证码模板", diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/AbpIdentityDomainModule.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/AbpIdentityDomainModule.cs index 7ed5c893e..9ff9e40e7 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/AbpIdentityDomainModule.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/AbpIdentityDomainModule.cs @@ -24,7 +24,8 @@ public class AbpIdentityDomainModule : AbpModule { PreConfigure(builder => { - builder.AddUserValidator(); + builder.AddUserValidator() + .AddPasswordValidator(); }); } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/PasswordHistoryPasswordValidator.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/PasswordHistoryPasswordValidator.cs new file mode 100644 index 000000000..99986fb29 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/PasswordHistoryPasswordValidator.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Identity.Localization; +using Volo.Abp.Identity.Settings; +using Volo.Abp.Settings; +using IdentityUser = Volo.Abp.Identity.IdentityUser; + +namespace Microsoft.AspNetCore.Identity; + +public class PasswordHistoryPasswordValidator : IPasswordValidator +{ + public async virtual Task ValidateAsync(UserManager manager, IdentityUser user, string password) + { + var settingProvider = manager.ServiceProvider.GetRequiredService(); + if (await settingProvider.IsTrueAsync(IdentitySettingNames.Password.EnablePreventPasswordReuse)) + { + var preventPasswordReuseCount = await settingProvider.GetAsync(IdentitySettingNames.Password.PreventPasswordReuseCount, 6); + if (preventPasswordReuseCount > 0 && user.PasswordHistories.Any(x => x.Password == password)) + { + var localizer = manager.ServiceProvider.GetRequiredService>(); + + return IdentityResult.Failed(new IdentityError + { + Code = LINGYUN.Abp.Identity.IdentityErrorCodes.PasswordInHistoryInValid, + Description = localizer["Volo.Abp.Identity:PasswordInHistory", preventPasswordReuseCount] + }); + } + } + return IdentityResult.Success; + } +}