From 6ebbdd42a4f83a14ef22eba3539ceaa8256951a7 Mon Sep 17 00:00:00 2001 From: cKey <35512826+colinin@users.noreply.github.com> Date: Thu, 19 Nov 2020 13:39:54 +0800 Subject: [PATCH] The SMS verification code should be generated through the user security token --- .../LINGYUN.MicroService.IdentityServer.sln | 45 +- ...NGYUN.MicroService.IdentityServerAdmin.sln | 30 +- ...N.Abp.Account.Application.Contracts.csproj | 10 +- .../AbpAccountApplicationContractsModule.cs | 22 +- ...mberRegisterDto.cs => PhoneRegisterDto.cs} | 16 +- ...rdResetDto.cs => PhoneResetPasswordDto.cs} | 12 +- ...rifyDto.cs => SendPhoneRegisterCodeDto.cs} | 6 +- .../Dto/SendPhoneResetPasswordCodeDto.cs | 15 + ...gisterDto.cs => SendPhoneSigninCodeDto.cs} | 3 +- .../Abp/Account/Dto/WeChatRegisterDto.cs | 11 +- .../LINGYUN/Abp/Account/IAccountAppService.cs | 44 +- .../Account/Localization/Resources/en.json | 15 + .../Localization/Resources/zh-Hans.json | 15 + .../LINGYUN.Abp.Account.Application.csproj | 2 +- .../Account/AbpAccountApplicationModule.cs | 5 +- .../LINGYUN/Abp/Account/AccountAppService.cs | 402 ++++++------- .../LINGYUN.Abp.Account.Domain.Shared.csproj | 12 +- .../AccountSettingDefinitionProvider.cs | 63 -- .../Abp/Account/AccountSettingNames.cs | 23 - .../Account/Localization/AccountResource.cs | 9 - .../Account/Localization/Resources/en.json | 20 - .../Localization/Resources/zh-Hans.json | 20 - .../LINGYUN.Abp.Account.Domain.csproj | 2 +- .../Identity/PhoneNumberUserValidator.cs | 50 -- .../LINGYUN.Abp.Account.HttpApi.csproj | 2 +- .../Abp/Account/AbpAccountHttpApiModule.cs | 20 +- .../LINGYUN/Abp/Account/AccountController.cs | 37 +- ...NGYUN.Abp.AspNetCore.Mvc.Validation.csproj | 24 + .../AbpAspNetCoreMvcValidationModule.cs | 31 + .../AbpDataAnnotationsMetadataProvider.cs | 563 ++++++++++++++++++ .../AbpLocalizerModelMetadataProvider.cs | 69 +++ ...AutoLocalizationMetadataDetailsProvider.cs | 69 +++ .../Localization/MissingFields/en.json | 7 + .../Localization/MissingFields/zh-Hans.json | 7 + ...dressInput.cs => ChangeEmailAddressDto.cs} | 7 +- ...NumberInput.cs => ChangePhoneNumberDto.cs} | 7 +- ...ledDto.cs => ChangeTwoFactorEnabledDto.cs} | 2 +- .../Dto/SendChangeEmailAddressCodeDto.cs | 18 + .../Dto/SendChangePhoneNumberCodeDto.cs | 18 + .../Abp/Identity/IIdentityUserAppService.cs | 2 +- .../Abp/Identity/IMyProfileAppService.cs | 19 +- .../Abp/Identity/IdentityUserAppService.cs | 2 +- .../Abp/Identity/MyProfileAppService.cs | 69 ++- .../Abp/Identity/IUserSecurityCodeSender.cs | 20 + .../Abp/Identity/IdentityErrorCodes.cs | 8 + .../LINGYUN/Abp/Identity/Localization/en.json | 20 +- .../Abp/Identity/Localization/zh-Hans.json | 19 +- .../Identity/Security/DefaultTotpService.cs | 121 ++++ .../Abp/Identity/Security/ITotpService.cs | 12 + .../IdentitySettingDefinitionProvider.cs | 52 ++ .../Identity/Settings/IdentitySettingNames.cs | 25 + .../Abp/Identity/IIdentityUserRepository.cs | 68 ++- .../Abp/Identity/SmsSecurityTokenCacheItem.cs | 39 ++ .../Identity/PhoneNumberUserValidator.cs | 57 ++ .../EfCoreIdentityUserRepository.cs | 38 ++ .../Abp/Identity/AbpIdentityHttpApiModule.cs | 8 + .../Abp/Identity/IdentityUserController.cs | 2 +- .../Abp/Identity/MyProfileController.cs | 16 +- .../AbpIdentityServerHttpApiModule.cs | 6 + ...YUN.Abp.IdentityServer.SmsValidator.csproj | 2 +- .../SmsValidator/SmsTokenGrantValidator.cs | 13 +- .../AuthIdentityServerModule.cs | 8 +- .../AuthServer.Host/AuthServer.Host.csproj | 5 +- .../EfCoreIdentityUserExtensionRepository.cs | 52 -- .../Pages/Account/SendCode.cshtml.cs | 5 +- ...AbpIdentityServerAdminHttpApiHostModule.cs | 12 +- .../Emailing/Templates/EmailConfirmed.tpl | 5 + ...IdentityEmailTemplateDefinitionProvider.cs | 26 + .../Templates/IdentityEmailTemplates.cs | 7 + .../EfCoreIdentityUserExtensionRepository.cs | 42 -- ...UN.Abp.IdentityServer4.HttpApi.Host.csproj | 11 + .../Properties/launchSettings.json | 14 +- .../UserSecurityCodeSender.cs | 66 ++ vueJs/src/api/users.ts | 35 +- vueJs/src/views/login/index.vue | 7 +- vueJs/src/views/register/index.vue | 7 +- vueJs/src/views/reset-password/index.vue | 7 +- 77 files changed, 2005 insertions(+), 655 deletions(-) rename aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/{PhoneNumberRegisterDto.cs => PhoneRegisterDto.cs} (69%) rename aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/{PasswordResetDto.cs => PhoneResetPasswordDto.cs} (50%) rename aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/{VerifyDto.cs => SendPhoneRegisterCodeDto.cs} (75%) create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneResetPasswordCodeDto.cs rename aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/{RegisterDto.cs => SendPhoneSigninCodeDto.cs} (80%) create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json delete mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/AccountSettingDefinitionProvider.cs delete mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/AccountSettingNames.cs delete mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/AccountResource.cs delete mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/Resources/en.json delete mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json delete mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Domain/Microsoft/AspNetCore/Identity/PhoneNumberUserValidator.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN.Abp.AspNetCore.Mvc.Validation.csproj create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpAspNetCoreMvcValidationModule.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpDataAnnotationsMetadataProvider.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpLocalizerModelMetadataProvider.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/DataAnnotationAutoLocalizationMetadataDetailsProvider.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields/en.json create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields/zh-Hans.json rename aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/{ChangeEmailAddressInput.cs => ChangeEmailAddressDto.cs} (75%) rename aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/{ChangePhoneNumberInput.cs => ChangePhoneNumberDto.cs} (75%) rename aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/{IdentityUserTwoFactorEnabledDto.cs => ChangeTwoFactorEnabledDto.cs} (65%) create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/SendChangeEmailAddressCodeDto.cs create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/SendChangePhoneNumberCodeDto.cs create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IUserSecurityCodeSender.cs create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Security/DefaultTotpService.cs create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Security/ITotpService.cs create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/SmsSecurityTokenCacheItem.cs create mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/PhoneNumberUserValidator.cs delete mode 100644 aspnet-core/services/account/AuthServer.Host/EntityFrameworkCore/Identity/EfCoreIdentityUserExtensionRepository.cs create mode 100644 aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/EmailConfirmed.tpl create mode 100644 aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/IdentityEmailTemplateDefinitionProvider.cs create mode 100644 aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/IdentityEmailTemplates.cs delete mode 100644 aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/EntityFrameworkCore/Identity/EfCoreIdentityUserExtensionRepository.cs create mode 100644 aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/UserSecurityCodeSender.cs diff --git a/aspnet-core/LINGYUN.MicroService.IdentityServer.sln b/aspnet-core/LINGYUN.MicroService.IdentityServer.sln index 7315746d7..12c2d55a8 100644 --- a/aspnet-core/LINGYUN.MicroService.IdentityServer.sln +++ b/aspnet-core/LINGYUN.MicroService.IdentityServer.sln @@ -23,12 +23,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "wechat", "wechat", "{AF8AEC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WeChat.Authorization", "modules\wechat\LINGYUN.Abp.WeChat.Authorization\LINGYUN.Abp.WeChat.Authorization.csproj", "{F656C1B2-6122-44C1-97F4-421CB4C03ED8}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "account", "account", "{09C7BFF7-BCCB-4E02-8DAE-3B047A68011D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Account.Domain.Shared", "modules\account\LINGYUN.Abp.Account.Domain.Shared\LINGYUN.Abp.Account.Domain.Shared.csproj", "{495A110A-8034-409B-A036-28CDF2CA9386}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Account.Domain", "modules\account\LINGYUN.Abp.Account.Domain\LINGYUN.Abp.Account.Domain.csproj", "{650FE9CC-4A55-4F2C-A82C-EF10DEB32DF0}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "permission-management", "permission-management", "{2FEA83BA-2E6D-40AF-8781-12CFD042457F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.PermissionManagement.Domain", "modules\permissions-management\LINGYUN.Abp.PermissionManagement.Domain\LINGYUN.Abp.PermissionManagement.Domain.csproj", "{A0A11373-635B-4343-8EE7-1DFC3611F05A}" @@ -49,6 +43,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Aliyun.Authoriz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Sms.Aliyun", "modules\common\LINGYUN.Abp.Sms.Aliyun\LINGYUN.Abp.Sms.Aliyun.csproj", "{A51E2EB5-EEC0-4FB0-99B9-A0FBE0D8B0EB}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.Domain.Shared", "modules\identity\LINGYUN.Abp.Identity.Domain.Shared\LINGYUN.Abp.Identity.Domain.Shared.csproj", "{E5E51981-864A-4B54-8E50-B5AE8B81245C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.Domain", "modules\identity\LINGYUN.Abp.Identity.Domain\LINGYUN.Abp.Identity.Domain.csproj", "{6514E12E-D97C-4311-8301-F270D4E04A9F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.EntityFrameworkCore", "modules\identity\LINGYUN.Abp.Identity.EntityFrameworkCore\LINGYUN.Abp.Identity.EntityFrameworkCore.csproj", "{77F424CB-75F2-4D7F-8B52-BBE2E09CB423}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.IdentityServer.EntityFrameworkCore", "modules\identityServer\LINGYUN.Abp.IdentityServer.EntityFrameworkCore\LINGYUN.Abp.IdentityServer.EntityFrameworkCore.csproj", "{644FAB8F-ED83-4539-AF43-8F8FE61EF778}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,14 +77,6 @@ Global {F656C1B2-6122-44C1-97F4-421CB4C03ED8}.Debug|Any CPU.Build.0 = Debug|Any CPU {F656C1B2-6122-44C1-97F4-421CB4C03ED8}.Release|Any CPU.ActiveCfg = Release|Any CPU {F656C1B2-6122-44C1-97F4-421CB4C03ED8}.Release|Any CPU.Build.0 = Release|Any CPU - {495A110A-8034-409B-A036-28CDF2CA9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {495A110A-8034-409B-A036-28CDF2CA9386}.Debug|Any CPU.Build.0 = Debug|Any CPU - {495A110A-8034-409B-A036-28CDF2CA9386}.Release|Any CPU.ActiveCfg = Release|Any CPU - {495A110A-8034-409B-A036-28CDF2CA9386}.Release|Any CPU.Build.0 = Release|Any CPU - {650FE9CC-4A55-4F2C-A82C-EF10DEB32DF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {650FE9CC-4A55-4F2C-A82C-EF10DEB32DF0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {650FE9CC-4A55-4F2C-A82C-EF10DEB32DF0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {650FE9CC-4A55-4F2C-A82C-EF10DEB32DF0}.Release|Any CPU.Build.0 = Release|Any CPU {A0A11373-635B-4343-8EE7-1DFC3611F05A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A0A11373-635B-4343-8EE7-1DFC3611F05A}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0A11373-635B-4343-8EE7-1DFC3611F05A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -111,6 +105,22 @@ Global {A51E2EB5-EEC0-4FB0-99B9-A0FBE0D8B0EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {A51E2EB5-EEC0-4FB0-99B9-A0FBE0D8B0EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {A51E2EB5-EEC0-4FB0-99B9-A0FBE0D8B0EB}.Release|Any CPU.Build.0 = Release|Any CPU + {E5E51981-864A-4B54-8E50-B5AE8B81245C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5E51981-864A-4B54-8E50-B5AE8B81245C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5E51981-864A-4B54-8E50-B5AE8B81245C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5E51981-864A-4B54-8E50-B5AE8B81245C}.Release|Any CPU.Build.0 = Release|Any CPU + {6514E12E-D97C-4311-8301-F270D4E04A9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6514E12E-D97C-4311-8301-F270D4E04A9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6514E12E-D97C-4311-8301-F270D4E04A9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6514E12E-D97C-4311-8301-F270D4E04A9F}.Release|Any CPU.Build.0 = Release|Any CPU + {77F424CB-75F2-4D7F-8B52-BBE2E09CB423}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77F424CB-75F2-4D7F-8B52-BBE2E09CB423}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77F424CB-75F2-4D7F-8B52-BBE2E09CB423}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77F424CB-75F2-4D7F-8B52-BBE2E09CB423}.Release|Any CPU.Build.0 = Release|Any CPU + {644FAB8F-ED83-4539-AF43-8F8FE61EF778}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {644FAB8F-ED83-4539-AF43-8F8FE61EF778}.Debug|Any CPU.Build.0 = Debug|Any CPU + {644FAB8F-ED83-4539-AF43-8F8FE61EF778}.Release|Any CPU.ActiveCfg = Release|Any CPU + {644FAB8F-ED83-4539-AF43-8F8FE61EF778}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -124,9 +134,6 @@ Global {F87B2BA3-40BC-4CB8-B53E-793C860F285D} = {70B8D735-1E89-4163-8EDB-56A74E374CF4} {AF8AECC8-0F42-4FC5-B3C0-00987BA8279F} = {7C727110-59CA-495B-8143-C5DDEDE41289} {F656C1B2-6122-44C1-97F4-421CB4C03ED8} = {AF8AECC8-0F42-4FC5-B3C0-00987BA8279F} - {09C7BFF7-BCCB-4E02-8DAE-3B047A68011D} = {7C727110-59CA-495B-8143-C5DDEDE41289} - {495A110A-8034-409B-A036-28CDF2CA9386} = {09C7BFF7-BCCB-4E02-8DAE-3B047A68011D} - {650FE9CC-4A55-4F2C-A82C-EF10DEB32DF0} = {09C7BFF7-BCCB-4E02-8DAE-3B047A68011D} {2FEA83BA-2E6D-40AF-8781-12CFD042457F} = {7C727110-59CA-495B-8143-C5DDEDE41289} {A0A11373-635B-4343-8EE7-1DFC3611F05A} = {2FEA83BA-2E6D-40AF-8781-12CFD042457F} {6F13C138-1D71-48F5-A404-94047242E98F} = {AF8AECC8-0F42-4FC5-B3C0-00987BA8279F} @@ -137,6 +144,10 @@ Global {6CB9D0F3-8D50-49B5-98E2-D777717E51E8} = {7C727110-59CA-495B-8143-C5DDEDE41289} {C675C7DB-5825-4F64-BF76-8A4452A448D2} = {572DBB40-4637-4C01-8491-8686F7E22B45} {A51E2EB5-EEC0-4FB0-99B9-A0FBE0D8B0EB} = {572DBB40-4637-4C01-8491-8686F7E22B45} + {E5E51981-864A-4B54-8E50-B5AE8B81245C} = {70B8D735-1E89-4163-8EDB-56A74E374CF4} + {6514E12E-D97C-4311-8301-F270D4E04A9F} = {70B8D735-1E89-4163-8EDB-56A74E374CF4} + {77F424CB-75F2-4D7F-8B52-BBE2E09CB423} = {70B8D735-1E89-4163-8EDB-56A74E374CF4} + {644FAB8F-ED83-4539-AF43-8F8FE61EF778} = {98887A8F-7040-4FA1-842F-A4C77A61ED09} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FCB77471-9ECB-4666-A316-1D6A6285A468} diff --git a/aspnet-core/LINGYUN.MicroService.IdentityServerAdmin.sln b/aspnet-core/LINGYUN.MicroService.IdentityServerAdmin.sln index 043bf0498..087d6dc2a 100644 --- a/aspnet-core/LINGYUN.MicroService.IdentityServerAdmin.sln +++ b/aspnet-core/LINGYUN.MicroService.IdentityServerAdmin.sln @@ -25,18 +25,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "identity-server", "identity EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "identity", "identity", "{BD964040-90B2-4179-A5EB-5830F5C7E073}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Account.Domain.Shared", "modules\account\LINGYUN.Abp.Account.Domain.Shared\LINGYUN.Abp.Account.Domain.Shared.csproj", "{82ACBDDA-1C77-49B6-9F33-38DFD3D4691D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Account.Domain", "modules\account\LINGYUN.Abp.Account.Domain\LINGYUN.Abp.Account.Domain.csproj", "{2BA25A1B-D53E-432B-BAC2-BAAC66CA390D}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Account.Application.Contracts", "modules\account\LINGYUN.Abp.Account.Application.Contracts\LINGYUN.Abp.Account.Application.Contracts.csproj", "{A4DFDE21-1931-46B5-8381-36B6EB6E51E1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Account.Application", "modules\account\LINGYUN.Abp.Account.Application\LINGYUN.Abp.Account.Application.csproj", "{6EE4D85F-6035-4AB5-B650-9E3D3A2AA8D2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Account.HttpApi", "modules\account\LINGYUN.Abp.Account.HttpApi\LINGYUN.Abp.Account.HttpApi.csproj", "{BB1124C0-79F4-4E72-8854-945B3F0AD76D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Account.Web", "modules\account\LINGYUN.Abp.Account.Web\LINGYUN.Abp.Account.Web.csproj", "{3297ED1B-FB82-4EC5-ADA1-0D0390BEDBB8}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.Domain", "modules\identity\LINGYUN.Abp.Identity.Domain\LINGYUN.Abp.Identity.Domain.csproj", "{E22445B9-7039-4DA1-B547-BAB437D6D2B3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.Application.Contracts", "modules\identity\LINGYUN.Abp.Identity.Application.Contracts\LINGYUN.Abp.Identity.Application.Contracts.csproj", "{116D9D69-ED2C-4F23-9445-3981D8EA0EB7}" @@ -59,7 +53,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{FD2DDD EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Settings", "modules\common\LINGYUN.Abp.Settings\LINGYUN.Abp.Settings.csproj", "{A8C83B9C-C9BF-48EF-80FF-D4FE2C936EFC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Identity.Domain.Shared", "modules\identity\LINGYUN.Abp.Identity.Domain.Shared\LINGYUN.Abp.Identity.Domain.Shared.csproj", "{23536755-4F00-4929-9C5E-D4CABD1CC513}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.Domain.Shared", "modules\identity\LINGYUN.Abp.Identity.Domain.Shared\LINGYUN.Abp.Identity.Domain.Shared.csproj", "{23536755-4F00-4929-9C5E-D4CABD1CC513}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.ExceptionHandling", "modules\common\LINGYUN.Abp.ExceptionHandling\LINGYUN.Abp.ExceptionHandling.csproj", "{80A418EB-6149-4684-80EF-D8574B91FE2B}" EndProject @@ -79,6 +73,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.MultiTenancy", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.MultiTenancy.DbFinder", "modules\tenants\LINGYUN.Abp.MultiTenancy.DbFinder\LINGYUN.Abp.MultiTenancy.DbFinder.csproj", "{70C4FA43-0526-48E3-B852-A21395502604}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.AspNetCore.Mvc.Validation", "modules\common\LINGYUN.Abp.AspNetCore.Mvc.Validation\LINGYUN.Abp.AspNetCore.Mvc.Validation.csproj", "{B3181D37-F379-4E16-8621-5E739C519393}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -109,14 +105,6 @@ Global {DE9E58E2-268C-4DF0-85AA-FF7F328B62F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {DE9E58E2-268C-4DF0-85AA-FF7F328B62F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {DE9E58E2-268C-4DF0-85AA-FF7F328B62F0}.Release|Any CPU.Build.0 = Release|Any CPU - {82ACBDDA-1C77-49B6-9F33-38DFD3D4691D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {82ACBDDA-1C77-49B6-9F33-38DFD3D4691D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {82ACBDDA-1C77-49B6-9F33-38DFD3D4691D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {82ACBDDA-1C77-49B6-9F33-38DFD3D4691D}.Release|Any CPU.Build.0 = Release|Any CPU - {2BA25A1B-D53E-432B-BAC2-BAAC66CA390D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2BA25A1B-D53E-432B-BAC2-BAAC66CA390D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BA25A1B-D53E-432B-BAC2-BAAC66CA390D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2BA25A1B-D53E-432B-BAC2-BAAC66CA390D}.Release|Any CPU.Build.0 = Release|Any CPU {A4DFDE21-1931-46B5-8381-36B6EB6E51E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4DFDE21-1931-46B5-8381-36B6EB6E51E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4DFDE21-1931-46B5-8381-36B6EB6E51E1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -129,10 +117,6 @@ Global {BB1124C0-79F4-4E72-8854-945B3F0AD76D}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB1124C0-79F4-4E72-8854-945B3F0AD76D}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB1124C0-79F4-4E72-8854-945B3F0AD76D}.Release|Any CPU.Build.0 = Release|Any CPU - {3297ED1B-FB82-4EC5-ADA1-0D0390BEDBB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3297ED1B-FB82-4EC5-ADA1-0D0390BEDBB8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3297ED1B-FB82-4EC5-ADA1-0D0390BEDBB8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3297ED1B-FB82-4EC5-ADA1-0D0390BEDBB8}.Release|Any CPU.Build.0 = Release|Any CPU {E22445B9-7039-4DA1-B547-BAB437D6D2B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E22445B9-7039-4DA1-B547-BAB437D6D2B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {E22445B9-7039-4DA1-B547-BAB437D6D2B3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -205,6 +189,10 @@ Global {70C4FA43-0526-48E3-B852-A21395502604}.Debug|Any CPU.Build.0 = Debug|Any CPU {70C4FA43-0526-48E3-B852-A21395502604}.Release|Any CPU.ActiveCfg = Release|Any CPU {70C4FA43-0526-48E3-B852-A21395502604}.Release|Any CPU.Build.0 = Release|Any CPU + {B3181D37-F379-4E16-8621-5E739C519393}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3181D37-F379-4E16-8621-5E739C519393}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3181D37-F379-4E16-8621-5E739C519393}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3181D37-F379-4E16-8621-5E739C519393}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -219,12 +207,9 @@ Global {4F06C65B-22CC-466E-B3FD-3F695DD38191} = {14B8F528-C649-4FAD-9BBB-6C979ED403E1} {7AB942D5-D139-4F9F-9342-71534AA3A5AC} = {14B8F528-C649-4FAD-9BBB-6C979ED403E1} {BD964040-90B2-4179-A5EB-5830F5C7E073} = {14B8F528-C649-4FAD-9BBB-6C979ED403E1} - {82ACBDDA-1C77-49B6-9F33-38DFD3D4691D} = {4F06C65B-22CC-466E-B3FD-3F695DD38191} - {2BA25A1B-D53E-432B-BAC2-BAAC66CA390D} = {4F06C65B-22CC-466E-B3FD-3F695DD38191} {A4DFDE21-1931-46B5-8381-36B6EB6E51E1} = {4F06C65B-22CC-466E-B3FD-3F695DD38191} {6EE4D85F-6035-4AB5-B650-9E3D3A2AA8D2} = {4F06C65B-22CC-466E-B3FD-3F695DD38191} {BB1124C0-79F4-4E72-8854-945B3F0AD76D} = {4F06C65B-22CC-466E-B3FD-3F695DD38191} - {3297ED1B-FB82-4EC5-ADA1-0D0390BEDBB8} = {4F06C65B-22CC-466E-B3FD-3F695DD38191} {E22445B9-7039-4DA1-B547-BAB437D6D2B3} = {BD964040-90B2-4179-A5EB-5830F5C7E073} {116D9D69-ED2C-4F23-9445-3981D8EA0EB7} = {BD964040-90B2-4179-A5EB-5830F5C7E073} {B07A1763-1B37-4416-8F0C-B938C175A56D} = {BD964040-90B2-4179-A5EB-5830F5C7E073} @@ -246,6 +231,7 @@ Global {89BA9708-62E5-4FD6-A28A-CF9E1C26DCAE} = {14B8F528-C649-4FAD-9BBB-6C979ED403E1} {99B60A42-29DA-4D91-9255-B90F53BC6D6E} = {89BA9708-62E5-4FD6-A28A-CF9E1C26DCAE} {70C4FA43-0526-48E3-B852-A21395502604} = {89BA9708-62E5-4FD6-A28A-CF9E1C26DCAE} + {B3181D37-F379-4E16-8621-5E739C519393} = {FD2DDD48-8F84-4924-BBAF-52080AB32267} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {785FFF4D-BC59-499E-88A3-7CB7A7667228} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN.Abp.Account.Application.Contracts.csproj b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN.Abp.Account.Application.Contracts.csproj index 9aecfae64..e2ec82337 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN.Abp.Account.Application.Contracts.csproj +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN.Abp.Account.Application.Contracts.csproj @@ -8,11 +8,17 @@ - + + - + + + + + + diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/AbpAccountApplicationContractsModule.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/AbpAccountApplicationContractsModule.cs index 50ff26017..4cff95a7d 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/AbpAccountApplicationContractsModule.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/AbpAccountApplicationContractsModule.cs @@ -1,9 +1,27 @@ -using Volo.Abp.Modularity; +using Volo.Abp.Account.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; namespace LINGYUN.Abp.Account { - [DependsOn(typeof(AbpAccountDomainSharedModule))] + [DependsOn( + typeof(Volo.Abp.Account.AbpAccountApplicationContractsModule))] public class AbpAccountApplicationContractsModule : AbpModule { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/Account/Localization/Resources"); + }); + } } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PhoneNumberRegisterDto.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PhoneRegisterDto.cs similarity index 69% rename from aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PhoneNumberRegisterDto.cs rename to aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PhoneRegisterDto.cs index fed5af779..3f11ff3b4 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PhoneNumberRegisterDto.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PhoneRegisterDto.cs @@ -1,35 +1,43 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using Volo.Abp.Auditing; using Volo.Abp.Identity; using Volo.Abp.Validation; namespace LINGYUN.Abp.Account { - public class PhoneNumberRegisterDto + public class PhoneRegisterDto { [Required] [Phone] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] + [Display(Name = "PhoneNumber")] public string PhoneNumber { get; set; } [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxNameLength))] + [DisplayName("Name")] public string Name { get; set; } [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))] + [DisplayName("UserName")] public string UserName { get; set; } [EmailAddress] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] + [DisplayName("EmailAddress")] public string EmailAddress { get; set; } [Required] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] [DataType(DataType.Password)] + [DisplayName("Password")] [DisableAuditing] public string Password { get; set; } [Required] - [StringLength(6)] - public string VerifyCode { get; set; } + [StringLength(6,MinimumLength = 6)] + [DisableAuditing] + [DisplayName("DisplayName:SmsVerifyCode")] + public string Code { get; set; } } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PasswordResetDto.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PhoneResetPasswordDto.cs similarity index 50% rename from aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PasswordResetDto.cs rename to aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PhoneResetPasswordDto.cs index 899323137..b698d7492 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PasswordResetDto.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/PhoneResetPasswordDto.cs @@ -5,11 +5,17 @@ using Volo.Abp.Validation; namespace LINGYUN.Abp.Account { - public class PasswordResetDto + public class PhoneResetPasswordDto { [Required] [Phone] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] + + // 如果Dto属性和本地化内容不一致,需要指定本地化名称如下 + // [Display(Name = "DisplayName:RequiredPhoneNumber")] //json本地化文件中必须有相同的格式: DisplayName:RequiredPhoneNumber + //[DisplayName("DisplayName:RequiredPhoneNumber")] //两种方法都可以 + + // 如果Dto属性与本地化内容一致,不需要显示指定名称,但是本地化文件必须存在对应格式的文本: DisplayName:PhoneNumber public string PhoneNumber { get; set; } [Required] @@ -20,6 +26,8 @@ namespace LINGYUN.Abp.Account [Required] [StringLength(6)] - public string VerifyCode { get; set; } + [DisableAuditing] + [Display(Name = "DisplayName:SmsVerifyCode")] + public string Code { get; set; } } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/VerifyDto.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneRegisterCodeDto.cs similarity index 75% rename from aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/VerifyDto.cs rename to aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneRegisterCodeDto.cs index 7ce34f592..29411035f 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/VerifyDto.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneRegisterCodeDto.cs @@ -4,14 +4,12 @@ using Volo.Abp.Validation; namespace LINGYUN.Abp.Account { - public class VerifyDto + public class SendPhoneRegisterCodeDto { [Required] [Phone] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] + [Display(Name = "PhoneNumber")] public string PhoneNumber { get; set; } - - [Required] - public PhoneNumberVerifyType VerifyType { get; set; } } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneResetPasswordCodeDto.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneResetPasswordCodeDto.cs new file mode 100644 index 000000000..f91daef98 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneResetPasswordCodeDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Identity; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.Account +{ + public class SendPhoneResetPasswordCodeDto + { + [Required] + [Phone] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] + [Display(Name = "PhoneNumber")] + public string PhoneNumber { get; set; } + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/RegisterDto.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneSigninCodeDto.cs similarity index 80% rename from aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/RegisterDto.cs rename to aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneSigninCodeDto.cs index 196ecce5a..a5113755f 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/RegisterDto.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/SendPhoneSigninCodeDto.cs @@ -4,11 +4,12 @@ using Volo.Abp.Validation; namespace LINGYUN.Abp.Account { - public class RegisterDto + public class SendPhoneSigninCodeDto { [Required] [Phone] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] + [Display(Name = "PhoneNumber")] public string PhoneNumber { get; set; } } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/WeChatRegisterDto.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/WeChatRegisterDto.cs index 178ad11cd..9911d49ff 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/WeChatRegisterDto.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/WeChatRegisterDto.cs @@ -8,22 +8,23 @@ namespace LINGYUN.Abp.Account public class WeChatRegisterDto { [Required] + [DisableAuditing] + [Display(Name = "DisplayName:WeChatCode")] public string Code { get; set; } - [DisableAuditing] [DataType(DataType.Password)] [Required] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] + [DisableAuditing] + [Display(Name = "Password")] public string Password { get; set; } - [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxNameLength))] - public string Name { get; set; } - [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))] + [Display(Name = "UserName")] public string UserName { get; set; } - [EmailAddress] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] + [Display(Name = "EmailAddress")] public string EmailAddress { get; set; } } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/IAccountAppService.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/IAccountAppService.cs index 25d36948a..c299db0a3 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/IAccountAppService.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/IAccountAppService.cs @@ -1,17 +1,45 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; -using Volo.Abp.Identity; namespace LINGYUN.Abp.Account { public interface IAccountAppService : IApplicationService { - Task RegisterAsync(PhoneNumberRegisterDto input); - - Task RegisterAsync(WeChatRegisterDto input); - - Task ResetPasswordAsync(PasswordResetDto input); - - Task VerifyPhoneNumberAsync(VerifyDto input); + /// + /// 通过手机号注册用户账户 + /// + /// + /// + Task RegisterAsync(PhoneRegisterDto input); + /// + /// 通过微信注册用户账户 + /// + /// + /// + Task RegisterAsync(WeChatRegisterDto input); + /// + /// 通过手机号重置用户密码 + /// + /// + /// + Task ResetPasswordAsync(PhoneResetPasswordDto input); + /// + /// 发送手机注册验证码短信 + /// + /// + /// + Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input); + /// + /// 发送手机登录验证码短信 + /// + /// + /// + Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input); + /// + /// 发送手机重置密码验证码短信 + /// + /// + /// + Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input); } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json new file mode 100644 index 000000000..4027f88bd --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json @@ -0,0 +1,15 @@ +{ + "culture": "en", + "texts": { + "SendRepeatSmsVerifyCode": "Phone verification code cannot be sent repeatedly within {0} minutes!", + "DuplicatePhoneNumber": "The phone number already exists!", + "PhoneNumberNotRegisterd": "The registered mobile phone number is not registered!", + "InvalidSmsVerifyCode": "The phone verification code is invalid or expired!", + "RequiredEmailAddress": "Email address required", + "InvalidPhoneNumber": "Invalid phone number", + "DuplicateWeChat": "The wechat has been registered!", + "DisplayName:SmsVerifyCode": "SMS verification code", + "DisplayName:EmailVerifyCode": "Mail verification code", + "DisplayName:WeChatCode": "Wechat login code" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..61f172530 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "culture": "zh-Hans", + "texts": { + "SendRepeatSmsVerifyCode": "手机验证码不能在 {0} 分钟内重复发送!", + "DuplicatePhoneNumber": "手机号已经存在!", + "PhoneNumberNotRegisterd": "手机号码未注册!", + "InvalidSmsVerifyCode": "手机验证码无效或已经过期!", + "RequiredEmailAddress": "邮件地址必须输入", + "InvalidPhoneNumber": "手机号无效", + "DuplicateWeChat": "微信号已经注册过!", + "DisplayName:SmsVerifyCode": "短信验证码", + "DisplayName:EmailVerifyCode": "邮件验证码", + "DisplayName:WeChatCode": "微信登录凭证" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN.Abp.Account.Application.csproj b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN.Abp.Account.Application.csproj index 4fef853ca..9813dc753 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN.Abp.Account.Application.csproj +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN.Abp.Account.Application.csproj @@ -14,9 +14,9 @@ + - diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/AbpAccountApplicationModule.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/AbpAccountApplicationModule.cs index c765eb16e..4fe51cb5d 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/AbpAccountApplicationModule.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/AbpAccountApplicationModule.cs @@ -1,12 +1,13 @@ -using LINGYUN.Abp.WeChat.Authorization; +using LINGYUN.Abp.Identity; +using LINGYUN.Abp.WeChat.Authorization; using Volo.Abp.Modularity; namespace LINGYUN.Abp.Account { [DependsOn( - typeof(AbpAccountDomainModule), typeof(Volo.Abp.Account.AbpAccountApplicationModule), typeof(AbpAccountApplicationContractsModule), + typeof(AbpIdentityDomainModule), typeof(AbpWeChatAuthorizationModule))] public class AbpAccountApplicationModule : AbpModule { 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 938ceaa01..9f53e47e3 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 @@ -1,49 +1,58 @@ -using LINGYUN.Abp.WeChat.Authorization; +using LINGYUN.Abp.Identity; +using LINGYUN.Abp.Identity.Security; +using LINGYUN.Abp.Identity.Settings; +using LINGYUN.Abp.WeChat.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using System; +using System.ComponentModel.DataAnnotations; +using System.Text; using System.Threading.Tasks; using Volo.Abp; +using Volo.Abp.Account.Localization; using Volo.Abp.Application.Services; using Volo.Abp.Caching; using Volo.Abp.Identity; using Volo.Abp.Settings; -using Volo.Abp.Sms; +using Volo.Abp.Validation; +using IIdentityUserRepository = LINGYUN.Abp.Identity.IIdentityUserRepository; namespace LINGYUN.Abp.Account { - /// - /// 用户注册服务 - /// public class AccountAppService : ApplicationService, IAccountAppService { - private IWeChatOpenIdFinder _weChatOpenIdFinder; - protected IWeChatOpenIdFinder WeChatOpenIdFinder => LazyGetRequiredService(ref _weChatOpenIdFinder); - protected ISmsSender SmsSender { get; } - protected IdentityUserManager UserManager { get; } + protected ITotpService TotpService { get; } protected IdentityUserStore UserStore { get; } + protected IdentityUserManager UserManager { get; } protected IIdentityUserRepository UserRepository { get; } - protected IDistributedCache Cache { get; } - protected PhoneNumberTokenProvider PhoneNumberTokenProvider { get; } + protected IUserSecurityCodeSender SecurityCodeSender { get; } + protected IWeChatOpenIdFinder WeChatOpenIdFinder { get; } + + protected IDistributedCache SecurityTokenCache { get; } + public AccountAppService( - ISmsSender smsSender, - IdentityUserManager userManager, + ITotpService totpService, IdentityUserStore userStore, + IdentityUserManager userManager, + IWeChatOpenIdFinder weChatOpenIdFinder, IIdentityUserRepository userRepository, - IDistributedCache cache, - PhoneNumberTokenProvider phoneNumberTokenProvider) + IUserSecurityCodeSender securityCodeSender, + IDistributedCache securityTokenCache) { - Cache = cache; - SmsSender = smsSender; + TotpService = totpService; UserStore = userStore; UserManager = userManager; UserRepository = userRepository; - PhoneNumberTokenProvider = phoneNumberTokenProvider; - LocalizationResource = typeof(Localization.AccountResource); + WeChatOpenIdFinder = weChatOpenIdFinder; + SecurityCodeSender = securityCodeSender; + SecurityTokenCache = securityTokenCache; + + LocalizationResource = typeof(AccountResource); } - public virtual async Task RegisterAsync(WeChatRegisterDto input) + public virtual async Task RegisterAsync(WeChatRegisterDto input) { + ThowIfInvalidEmailAddress(input.EmailAddress); await CheckSelfRegistrationAsync(); var wehchatOpenId = await WeChatOpenIdFinder.FindAsync(input.Code); @@ -54,13 +63,19 @@ namespace LINGYUN.Abp.Account // 应该要抛出微信号已注册异常,而不是直接返回注册用户数据,否则造成用户信息泄露 throw new UserFriendlyException(L["DuplicateWeChat"]); } - var userName = input.UserName ?? "wx-" + wehchatOpenId.OpenId; - var userEmail = input.EmailAddress ?? $"{userName}@{CurrentTenant.Name ?? "default"}.io";//如果邮件地址不验证,随意写入一个 - - user = new IdentityUser(GuidGenerator.Create(), userName, userEmail, CurrentTenant.Id) + var userName = input.UserName; + if (userName.IsNullOrWhiteSpace()) + { + userName = "wxid-" + wehchatOpenId.OpenId.ToMd5(); + } + + var userEmail = input.EmailAddress;//如果邮件地址不验证,随意写入一个 + if (userEmail.IsNullOrWhiteSpace()) { - Name = input.Name ?? userName - }; + userEmail = $"{userName}@{CurrentTenant.Name ?? "default"}.io"; + } + + user = new IdentityUser(GuidGenerator.Create(), userName, userEmail, CurrentTenant.Id); (await UserManager.CreateAsync(user, input.Password)).CheckErrors(); (await UserManager.AddDefaultRolesAsync(user)).CheckErrors(); @@ -68,205 +83,192 @@ namespace LINGYUN.Abp.Account var userLogin = new UserLoginInfo(AbpWeChatAuthorizationConsts.ProviderKey, wehchatOpenId.OpenId, AbpWeChatAuthorizationConsts.DisplayName); (await UserManager.AddLoginAsync(user, userLogin)).CheckErrors(); - return ObjectMapper.Map(user); + await CurrentUnitOfWork.SaveChangesAsync(); } - /// - /// 用户注册 - /// - /// - /// - /// - /// 用户通过VerifyPhoneNumber接口发送到手机的验证码,传递注册信息注册用户 - /// 如果没有此手机号的缓存记录或验证码不匹配,抛出验证码无效的异常 - /// 用户注册成功,清除缓存的验证码记录 - /// - public virtual async Task RegisterAsync(PhoneNumberRegisterDto input) + + public virtual async Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input) { - var phoneVerifyCacheKey = NormalizeCacheKey(input.PhoneNumber); + await CheckSelfRegistrationAsync(); + await CheckNewUserPhoneNumberNotBeUsedAsync(input.PhoneNumber); - var phoneVerifyCacheItem = await Cache.GetAsync(phoneVerifyCacheKey); - if(phoneVerifyCacheItem == null || !phoneVerifyCacheItem.VerifyCode.Equals(input.VerifyCode)) + var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.PhoneNumber, "SmsVerifyCode"); + var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); + var interval = await SettingProvider.GetAsync(IdentitySettingNames.User.SmsRepetInterval, 1); + + if (securityTokenCacheItem != null) { - throw new UserFriendlyException(L["PhoneVerifyCodeInvalid"]); + throw new UserFriendlyException(L["SendRepeatSmsVerifyCode", interval]); } - await CheckSelfRegistrationAsync(); + var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsNewUserRegister); - // 需要用户输入邮箱? - //if (UserManager.Options.User.RequireUniqueEmail) - //{ - // if (input.EmailAddress.IsNullOrWhiteSpace()) - // { - // throw new UserFriendlyException(L["RequiredEmailAddress"]); - // } - //} - - var userEmail = input.EmailAddress ?? $"{input.PhoneNumber}@{CurrentTenant.Name ?? "default"}.io";//如果邮件地址不验证,随意写入一个 - var userName = input.UserName ?? input.PhoneNumber; - var user = new IdentityUser(GuidGenerator.Create(), userName, userEmail, CurrentTenant.Id) - { - Name = input.Name ?? input.PhoneNumber - }; - // 写入手机号要在创建用户之前,因为有一个自定义的手机号验证 - await UserStore.SetPhoneNumberAsync(user, input.PhoneNumber); - await UserStore.SetPhoneNumberConfirmedAsync(user, true); + // 安全令牌 + var securityToken = GuidGenerator.Create().ToString("N"); - (await UserManager.CreateAsync(user, input.Password)).CheckErrors(); + var code = TotpService.GenerateCode(Encoding.Unicode.GetBytes(securityToken), securityTokenCacheKey); + securityTokenCacheItem = new SmsSecurityTokenCacheItem(code.ToString(), securityToken); - (await UserManager.AddDefaultRolesAsync(user)).CheckErrors(); + await SecurityCodeSender.SendPhoneConfirmedCodeAsync( + input.PhoneNumber, securityTokenCacheItem.Token, template); - await Cache.RemoveAsync(phoneVerifyCacheKey); - - return ObjectMapper.Map(user); + await SecurityTokenCache + .SetAsync(securityTokenCacheKey, securityTokenCacheItem, + new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(interval) + }); } - // TODO: 是否有必要移动到ProfileService - /// - /// 重置用户密码 - /// - /// - /// - public virtual async Task ResetPasswordAsync(PasswordResetDto passwordReset) + public virtual async Task RegisterAsync(PhoneRegisterDto input) { - // 本来可以不需要的,令牌算法有一个有效期 - // 不过这里采用令牌强制过期策略,避免一个令牌多次使用 - var phoneVerifyCacheKey = NormalizeCacheKey(passwordReset.PhoneNumber); + await CheckSelfRegistrationAsync(); + await CheckNewUserPhoneNumberNotBeUsedAsync(input.PhoneNumber); - var phoneVerifyCacheItem = await Cache.GetAsync(phoneVerifyCacheKey); - if (phoneVerifyCacheItem == null || !phoneVerifyCacheItem.VerifyCode.Equals(passwordReset.VerifyCode)) + var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.PhoneNumber, "SmsVerifyCode"); + var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); + if (securityTokenCacheItem == null) { - throw new UserFriendlyException(L["PhoneVerifyCodeInvalid"]); + // 验证码过期 + throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); } - var userId = await GetUserIdByPhoneNumberAsync(passwordReset.PhoneNumber); + // 验证码是否有效 + if (input.Code.Equals(securityTokenCacheItem.Token) && int.TryParse(input.Code, out int token)) + { + var securityToken = Encoding.Unicode.GetBytes(securityTokenCacheItem.SecurityToken); + // 校验totp验证码 + if (TotpService.ValidateCode(securityToken, token, securityTokenCacheKey)) + { + var userEmail = input.EmailAddress ?? $"{input.PhoneNumber}@{CurrentTenant.Name ?? "default"}.io";//如果邮件地址不验证,随意写入一个 + var userName = input.UserName ?? input.PhoneNumber; + var user = new IdentityUser(GuidGenerator.Create(), userName, userEmail, CurrentTenant.Id) + { + Name = input.Name ?? input.PhoneNumber + }; + + await UserStore.SetPhoneNumberAsync(user, input.PhoneNumber); + await UserStore.SetPhoneNumberConfirmedAsync(user, true); - var user = await UserManager.GetByIdAsync(userId); + (await UserManager.CreateAsync(user, input.Password)).CheckErrors(); - (await UserManager.ResetPasswordAsync(user, phoneVerifyCacheItem.VerifyToken, passwordReset.NewPassword)).CheckErrors(); + (await UserManager.AddDefaultRolesAsync(user)).CheckErrors(); + await SecurityTokenCache.RemoveAsync(securityTokenCacheKey); - await Cache.RemoveAsync(phoneVerifyCacheKey); + await CurrentUnitOfWork.SaveChangesAsync(); + + return; + } + } + // 验证码无效 + throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); } - /// - /// 验证手机号码 - /// - /// - /// - /// - /// 用户传递手机号码及认证类型 - /// 1、如果认证类型为注册: - /// 先查询是否存在此手机号的缓存验证码信息,如果存在,抛出不能重复发送验证码异常 - /// 随机生成6位纯数字验证码,通过短信接口服务发送到用户手机,并缓存验证码,设定一个有效时长 - /// - /// 2、如果认证类型为登录: - /// 先查询是否存在此手机号的缓存验证码信息,如果存在,抛出不能重复发送验证码异常 - /// 通过手机号查询用户信息,如果用户不存在,抛出手机号未注册异常 - /// 调用PhoneNumberTokenProvider接口生成6位手机验证码,用途为 phone_verify - /// 发送手机验证码到用户手机,并缓存验证码,设定一个有效时长 - /// - /// 用户调用 IdentityServer4/connect/token 登录系统(需要引用LINGYUN.Abp.IdentityServer.SmsValidator模块) - /// 参数1:grant_type=phone_verify - /// 参数2:phone_number=手机号码 - /// 参数3:phone_verify_code=手机验证码 - /// 参数4:client_id=客户端标识 - /// 参数5:client_secret=客户端密钥 - /// - public virtual async Task VerifyPhoneNumberAsync(VerifyDto input) + + public virtual async Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input) { - // TODO: 借用TOTP算法生成6位动态验证码 + /* + * 注解: 微软的重置密码方法通过 UserManager.GeneratePasswordResetTokenAsync 接口生成密码重置Token + * 而这个Token设计的意义就是用户通过链接来重置密码,所以不适合短信验证 + * 某些企业是把链接生成一个短链发送短信的,不过这种方式不是很推荐,因为现在是真没几个人敢随便点短信链接的 + * + * 此处设计方式为: + * + * step1: 例行检查是否重复发送,这一点是很有必要的 + * step2: 通过已确认的手机号来查询用户,如果用户未确认手机号,那就不能发送,这一点也是很有必要的 + * step3(重点): 通过 UserManager.GenerateTwoFactorTokenAsync 接口来生成二次认证码,这就相当于伪验证码,只是用于确认用户传递的验证码是否通过 + * 比起自己生成随机数,这个验证码利用了TOTP算法,有时间限制的 + * step4(重点): 用户传递验证码后,通过 UserManager.VerifyTwoFactorTokenAsync 接口来校验验证码 + * 验证通过后,再利用 UserManager.GeneratePasswordResetTokenAsync 接口来生成真正的用于重置密码的Token + */ - var verifyCodeExpiration = await SettingProvider.GetAsync(AccountSettingNames.PhoneVerifyCodeExpiration); - var phoneVerifyCacheKey = NormalizeCacheKey(input.PhoneNumber); - var verifyCacheItem = await Cache.GetAsync(phoneVerifyCacheKey); - if (verifyCacheItem != null) + var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.PhoneNumber, "SmsVerifyCode"); + var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); + var interval = await SettingProvider.GetAsync(IdentitySettingNames.User.SmsRepetInterval, 1); + // 传递 isConfirmed 用户必须是已确认过手机号的 + var user = await GetUserByPhoneNumberAsync(input.PhoneNumber, isConfirmed: true); + // 能查询到缓存就是重复发送 + if (securityTokenCacheItem != null) { - throw new UserFriendlyException(L["PhoneVerifyCodeNotRepeatSend", verifyCodeExpiration]); + throw new UserFriendlyException(L["SendRepeatSmsVerifyCode", interval]); } - verifyCacheItem = new AccountRegisterVerifyCacheItem - { - PhoneNumber = input.PhoneNumber, - }; - switch (input.VerifyType) - { - case PhoneNumberVerifyType.Register: - var phoneVerifyCode = new Random().Next(100000, 999999); - verifyCacheItem.VerifyCode = phoneVerifyCode.ToString(); - var templateCode = await SettingProvider.GetOrDefaultAsync(AccountSettingNames.SmsRegisterTemplateCode, ServiceProvider); - await SendPhoneVerifyMessageAsync(templateCode, input.PhoneNumber, phoneVerifyCode.ToString()); - break; - case PhoneNumberVerifyType.Signin: - var phoneSigninCode = await SendSigninVerifyCodeAsync(input.PhoneNumber); - verifyCacheItem.VerifyCode = phoneSigninCode; - break; - case PhoneNumberVerifyType.ResetPassword: - var resetPasswordCode = new Random().Next(100000, 999999); - verifyCacheItem.VerifyCode = resetPasswordCode.ToString(); - var resetPasswordToken = await SendResetPasswordVerifyCodeAsync(input.PhoneNumber, verifyCacheItem.VerifyCode); - verifyCacheItem.VerifyToken = resetPasswordToken; - break; - } - - var cacheOptions = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(verifyCodeExpiration) - }; - await Cache.SetAsync(phoneVerifyCacheKey, verifyCacheItem, cacheOptions); - } - /// - /// 发送登录验证码 - /// - /// 手机号 - /// 返回登录验证码 - protected virtual async Task SendSigninVerifyCodeAsync(string phoneNumber) - { - // 查找用户信息 - var user = await GetUserByPhoneNumberAsync(phoneNumber); - // 获取登录验证码模板号 - var templateCode = await SettingProvider.GetOrDefaultAsync(AccountSettingNames.SmsSigninTemplateCode, ServiceProvider); - // 生成手机验证码 - var phoneVerifyCode = await PhoneNumberTokenProvider.GenerateAsync("phone_verify", UserManager, user); + var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsResetPassword); + // 生成二次认证码 + var code = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider); // 发送短信验证码 - await SendPhoneVerifyMessageAsync(templateCode, user.PhoneNumber, phoneVerifyCode); - - return phoneVerifyCode; + await SecurityCodeSender.SendPhoneConfirmedCodeAsync(input.PhoneNumber, code, template); + // 缓存这个手机号的记录,防重复 + securityTokenCacheItem = new SmsSecurityTokenCacheItem(code, user.SecurityStamp); + await SecurityTokenCache + .SetAsync(securityTokenCacheKey, securityTokenCacheItem, + new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(interval) + }); } - - protected virtual async Task SendResetPasswordVerifyCodeAsync(string phoneNumber, string phoneVerifyCode) + + public virtual async Task ResetPasswordAsync(PhoneResetPasswordDto input) { - // 查找用户信息 - var user = await GetUserByPhoneNumberAsync(phoneNumber); - // 获取登录验证码模板号 - var templateCode = await SettingProvider.GetOrDefaultAsync(AccountSettingNames.SmsResetPasswordTemplateCode, ServiceProvider); - // 生成重置密码验证码 - var phoneVerifyToken = await UserManager.GeneratePasswordResetTokenAsync(user); - // 发送短信验证码 - await SendPhoneVerifyMessageAsync(templateCode, user.PhoneNumber, phoneVerifyCode); + var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.PhoneNumber, "SmsVerifyCode"); + var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); + if (securityTokenCacheItem == null) + { + throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); + } + // 传递 isConfirmed 用户必须是已确认过手机号的 + var user = await GetUserByPhoneNumberAsync(input.PhoneNumber, isConfirmed: true); + // 验证二次认证码 + if (!await UserManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider, input.Code)) + { + // 验证码无效 + throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); + } + // 生成真正的重置密码Token + var resetPwdToken = await UserManager.GeneratePasswordResetTokenAsync(user); + // 重置密码 + (await UserManager.ResetPasswordAsync(user, resetPwdToken, input.NewPassword)).CheckErrors(); + // 移除缓存项 + await SecurityTokenCache.RemoveAsync(securityTokenCacheKey); - return phoneVerifyToken; + await CurrentUnitOfWork.SaveChangesAsync(); } - protected virtual async Task GetUserByPhoneNumberAsync(string phoneNumber) + public virtual async Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input) { - // 查找用户信息 - var user = await UserRepository.FindByPhoneNumberAsync(phoneNumber); - if (user == null) + var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.PhoneNumber, "SmsVerifyCode"); + var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); + var interval = await SettingProvider.GetAsync(IdentitySettingNames.User.SmsRepetInterval, 1); + if (securityTokenCacheItem != null) { - throw new UserFriendlyException(L["PhoneNumberNotRegisterd"]); + throw new UserFriendlyException(L["SendRepeatSmsVerifyCode", interval]); } - return user; + // 传递 isConfirmed 验证过的用户才允许通过手机登录 + var user = await GetUserByPhoneNumberAsync(input.PhoneNumber, isConfirmed: true); + var code = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider); + var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsUserSignin); + + // 发送登录验证码短信 + await SecurityCodeSender.SendPhoneConfirmedCodeAsync(input.PhoneNumber, code, template); + // 缓存登录验证码状态,防止同一手机号重复发送 + securityTokenCacheItem = new SmsSecurityTokenCacheItem(code, user.SecurityStamp); + await SecurityTokenCache + .SetAsync(securityTokenCacheKey, securityTokenCacheItem, + new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(interval) + }); } - protected virtual async Task GetUserIdByPhoneNumberAsync(string phoneNumber) + protected virtual async Task GetUserByPhoneNumberAsync(string phoneNumber, bool isConfirmed = true) { - // 查找用户信息 - var userId = await UserRepository.GetIdByPhoneNumberAsync(phoneNumber); - if (!userId.HasValue) + var user = await UserRepository.FindByPhoneNumberAsync(phoneNumber, isConfirmed, true); + if (user == null) { throw new UserFriendlyException(L["PhoneNumberNotRegisterd"]); } - return userId.Value; + return user; } + /// /// 检查是否允许用户注册 /// @@ -278,28 +280,26 @@ namespace LINGYUN.Abp.Account throw new UserFriendlyException(L["SelfRegistrationDisabledMessage"]); } } - /// - /// 发送短信验证码 - /// - /// - /// - /// - /// - protected virtual async Task SendPhoneVerifyMessageAsync(string templateCode, string phoneNumber, string verifyCode) + + protected virtual async Task CheckNewUserPhoneNumberNotBeUsedAsync(string phoneNumber) { - var sendMessage = new SmsMessage(phoneNumber, "SendSmsMessage"); - sendMessage.Properties.Add("code", verifyCode); - sendMessage.Properties.Add("TemplateCode", templateCode); - await SmsSender.SendAsync(sendMessage); + if (await UserRepository.IsPhoneNumberUedAsync(phoneNumber)) + { + throw new UserFriendlyException(L["DuplicatePhoneNumber"]); + } } - /// - /// 格式化缓存主键 - /// - /// 手机号码 - /// - protected string NormalizeCacheKey(string phoneNumber) + + private void ThowIfInvalidEmailAddress(string inputEmail) { - return $"ACCOUNT-PHONE:{phoneNumber}"; + if (!inputEmail.IsNullOrWhiteSpace() && + !ValidationHelper.IsValidEmailAddress(inputEmail)) + { + throw new AbpValidationException( + new ValidationResult[] + { + new ValidationResult(L["The {0} field is not a valid e-mail address.", L["DisplayName:EmailAddress"]], new string[]{ "EmailAddress" }) + }); + } } } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN.Abp.Account.Domain.Shared.csproj b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN.Abp.Account.Domain.Shared.csproj index 671875157..f34d4b2d2 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN.Abp.Account.Domain.Shared.csproj +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN.Abp.Account.Domain.Shared.csproj @@ -1,4 +1,4 @@ - + @@ -8,17 +8,11 @@ - - - - - - - + - + diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/AccountSettingDefinitionProvider.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/AccountSettingDefinitionProvider.cs deleted file mode 100644 index d4e3ccf0b..000000000 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/AccountSettingDefinitionProvider.cs +++ /dev/null @@ -1,63 +0,0 @@ -using LINGYUN.Abp.Account.Localization; -using Volo.Abp.Localization; -using Volo.Abp.Settings; - -namespace LINGYUN.Abp.Account -{ - public class AccountSettingDefinitionProvider : SettingDefinitionProvider - { - public override void Define(ISettingDefinitionContext context) - { - - context.Add(GetAccountSettings()); - } - - protected SettingDefinition[] GetAccountSettings() - { - return new SettingDefinition[] - { - new SettingDefinition( - name: AccountSettingNames.SmsRegisterTemplateCode, - defaultValue: "SMS_190728520", - displayName: L("DisplayName:SmsRegisterTemplateCode"), - description: L("Description:SmsRegisterTemplateCode"), - isVisibleToClients: true) - .WithProviders( - GlobalSettingValueProvider.ProviderName, - TenantSettingValueProvider.ProviderName), - new SettingDefinition( - name: AccountSettingNames.SmsSigninTemplateCode, - defaultValue: "SMS_190728516", - displayName: L("DisplayName:SmsSigninTemplateCode"), - description: L("Description:SmsSigninTemplateCode"), - isVisibleToClients: true) - .WithProviders( - GlobalSettingValueProvider.ProviderName, - TenantSettingValueProvider.ProviderName), - new SettingDefinition( - name: AccountSettingNames.SmsResetPasswordTemplateCode, - defaultValue: "SMS_192530831", - displayName: L("DisplayName:SmsResetPasswordTemplateCode"), - description: L("Description:SmsResetPasswordTemplateCode"), - isVisibleToClients: true) - .WithProviders( - GlobalSettingValueProvider.ProviderName, - TenantSettingValueProvider.ProviderName), - new SettingDefinition( - name: AccountSettingNames.PhoneVerifyCodeExpiration, - defaultValue: "3", - displayName: L("DisplayName:PhoneVerifyCodeExpiration"), - description: L("Description:PhoneVerifyCodeExpiration"), - isVisibleToClients: true) - .WithProviders( - GlobalSettingValueProvider.ProviderName, - TenantSettingValueProvider.ProviderName), - }; - } - - protected LocalizableString L(string name) - { - return LocalizableString.Create(name); - } - } -} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/AccountSettingNames.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/AccountSettingNames.cs deleted file mode 100644 index b5599c281..000000000 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/AccountSettingNames.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace LINGYUN.Abp.Account -{ - public class AccountSettingNames - { - public const string GroupName = "Abp.Account"; - /// - /// 短信验证码过期时间 - /// - public const string PhoneVerifyCodeExpiration = GroupName + ".PhoneVerifyCodeExpiration"; - /// - /// 用户注册短信验证码模板号 - /// - public const string SmsRegisterTemplateCode = GroupName + ".SmsRegisterTemplateCode"; - /// - /// 用户登录短信验证码模板号 - /// - public const string SmsSigninTemplateCode = GroupName + ".SmsSigninTemplateCode"; - /// - /// 用户重置密码短信验证码模板号 - /// - public const string SmsResetPasswordTemplateCode = GroupName + ".SmsResetPasswordTemplateCode"; - } -} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/AccountResource.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/AccountResource.cs deleted file mode 100644 index c132a9b9b..000000000 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/AccountResource.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Volo.Abp.Localization; - -namespace LINGYUN.Abp.Account.Localization -{ - [LocalizationResourceName("LINYUNAbpAccount")] - public class AccountResource - { - } -} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/Resources/en.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/Resources/en.json deleted file mode 100644 index 758961ad5..000000000 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/Resources/en.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "culture": "en", - "texts": { - "PhoneNumberNotRegisterd": "The registered mobile phone number is not registered!", - "PhoneVerifyCodeInvalid": "The phone verification code is invalid or expired!", - "PhoneVerifyCodeNotRepeatSend": "Phone verification code cannot be sent repeatedly within {0} minutes!", - "DisplayName:SmsRegisterTemplateCode": "Register sms template", - "Description:SmsRegisterTemplateCode": "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", - "DisplayName:SmsSigninTemplateCode": "Signin sms template", - "Description:SmsSigninTemplateCode": "When the user logs in, he/she should send the template number of the SMS verification code and fill in the template number of the corresponding cloud platform registration", - "DisplayName:SmsResetPasswordTemplateCode": "Reset password sms template", - "Description:SmsResetPasswordTemplateCode": "When the user resets the password, he/she sends the template number of SMS verification code and fills in the template number registered on the cloud platform", - "DisplayName:PhoneVerifyCodeExpiration": "SMS verification code validity", - "Description:PhoneVerifyCodeExpiration": "The valid time for the user to send SMS verification code, unit m, default 3m", - "RequiredEmailAddress": "Email address required", - "InvalidPhoneNumber": "Invalid phone number", - "DuplicatePhoneNumber": "The phone number {0} has been registered!", - "DuplicateWeChat": "The wechat has been registered!" - } -} \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json deleted file mode 100644 index da45e2944..000000000 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain.Shared/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "culture": "zh-Hans", - "texts": { - "PhoneNumberNotRegisterd": "手机号码未注册!", - "PhoneVerifyCodeInvalid": "手机验证码无效或已经过期!", - "PhoneVerifyCodeNotRepeatSend": "手机验证码不能在 {0} 分钟内重复发送!", - "DisplayName:SmsRegisterTemplateCode": "用户注册短信模板", - "Description:SmsRegisterTemplateCode": "用户注册时发送短信验证码的模板号,填写对应云平台注册的模板号", - "DisplayName:SmsSigninTemplateCode": "用户登录短信模板", - "Description:SmsSigninTemplateCode": "用户登录时发送短信验证码的模板号,填写对应云平台注册的模板号", - "DisplayName:SmsResetPasswordTemplateCode": "用户重置密码短信模板", - "Description:SmsResetPasswordTemplateCode": "用户重置密码时发送短信验证码的模板号,填写对应云平台注册的模板号", - "DisplayName:PhoneVerifyCodeExpiration": "短信验证码有效期", - "Description:PhoneVerifyCodeExpiration": "用户发送短信验证码的有效时长,单位m,默认3m", - "RequiredEmailAddress": "邮件地址必须输入", - "InvalidPhoneNumber": "手机号无效", - "DuplicatePhoneNumber": "手机号已经注册过!", - "DuplicateWeChat": "微信号已经注册过!" - } -} \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain/LINGYUN.Abp.Account.Domain.csproj b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain/LINGYUN.Abp.Account.Domain.csproj index 4381c4277..0272fba42 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain/LINGYUN.Abp.Account.Domain.csproj +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain/LINGYUN.Abp.Account.Domain.csproj @@ -12,7 +12,7 @@ - + diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain/Microsoft/AspNetCore/Identity/PhoneNumberUserValidator.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain/Microsoft/AspNetCore/Identity/PhoneNumberUserValidator.cs deleted file mode 100644 index 31a2237b4..000000000 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Domain/Microsoft/AspNetCore/Identity/PhoneNumberUserValidator.cs +++ /dev/null @@ -1,50 +0,0 @@ -using LINGYUN.Abp.Account.Localization; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using System; -using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Identity; -using IIdentityUserRepository = LINGYUN.Abp.Account.IIdentityUserRepository; - -namespace Microsoft.AspNetCore.Identity -{ - [Dependency(ServiceLifetime.Scoped, ReplaceServices = true)] - [ExposeServices(typeof(IUserValidator))] - public class PhoneNumberUserValidator : UserValidator - { - private readonly IStringLocalizer _stringLocalizer; - private readonly IIdentityUserRepository _identityUserRepository; - - public PhoneNumberUserValidator( - IStringLocalizer stringLocalizer, - IIdentityUserRepository identityUserRepository) - { - _stringLocalizer = stringLocalizer; - _identityUserRepository = identityUserRepository; - } - public override async Task ValidateAsync(UserManager manager, IdentityUser user) - { - await ValidatePhoneNumberAsync(manager, user); - return await base.ValidateAsync(manager, user); - } - - protected virtual async Task ValidatePhoneNumberAsync(UserManager manager, IdentityUser user) - { - var phoneNumber = await manager.GetPhoneNumberAsync(user); - if (phoneNumber.IsNullOrWhiteSpace()) - { - return; - // 如果用户没有手机号,不验证 - //throw new UserFriendlyException(_stringLocalizer["InvalidPhoneNumber"].Value, "InvalidPhoneNumber"); - } - - var phoneNumberHasRegisted = await _identityUserRepository.PhoneNumberHasRegistedAsync(phoneNumber); - if (phoneNumberHasRegisted) - { - throw new UserFriendlyException(_stringLocalizer["DuplicatePhoneNumber", phoneNumber].Value, "DuplicatePhoneNumber"); - } - } - } -} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN.Abp.Account.HttpApi.csproj b/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN.Abp.Account.HttpApi.csproj index 6ebc79af3..df3312e51 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN.Abp.Account.HttpApi.csproj +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN.Abp.Account.HttpApi.csproj @@ -8,7 +8,7 @@ - + diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AbpAccountHttpApiModule.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AbpAccountHttpApiModule.cs index 9ed5a4bc3..66ae75ed2 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AbpAccountHttpApiModule.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AbpAccountHttpApiModule.cs @@ -1,31 +1,27 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Account.Localization; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.Localization; +using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.Modularity; namespace LINGYUN.Abp.Account { [DependsOn( typeof(AbpAccountApplicationContractsModule), - typeof(AbpAspNetCoreMvcModule))] + typeof(Volo.Abp.Account.AbpAccountHttpApiModule))] public class AbpAccountHttpApiModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { - PreConfigure(mvcBuilder => + PreConfigure(options => { - mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpAccountHttpApiModule).Assembly); + options.AddAssemblyResource(typeof(AccountResource), typeof(AbpAccountApplicationContractsModule).Assembly); + // 原生的在Web项目指定,不没有引用Web项目的情况下需要它 + options.AddAssemblyResource(typeof(AccountResource), typeof(Volo.Abp.Account.AbpAccountApplicationContractsModule).Assembly); }); - } - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => + PreConfigure(mvcBuilder => { - options.Resources - .Get() - .AddBaseTypes(typeof(AccountResource)); + mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpAccountHttpApiModule).Assembly); }); } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AccountController.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AccountController.cs index 55d4db76d..e223617e6 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AccountController.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AccountController.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.Account; using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.Identity; namespace LINGYUN.Abp.Account { @@ -21,30 +20,44 @@ namespace LINGYUN.Abp.Account [HttpPost] [Route("wechat/register")] - public virtual async Task RegisterAsync(WeChatRegisterDto input) + public virtual async Task RegisterAsync(WeChatRegisterDto input) { - return await AccountAppService.RegisterAsync(input); + await AccountAppService.RegisterAsync(input); } [HttpPost] [Route("phone/register")] - public virtual async Task RegisterAsync(PhoneNumberRegisterDto input) + public virtual async Task RegisterAsync(PhoneRegisterDto input) { - return await AccountAppService.RegisterAsync(input); + await AccountAppService.RegisterAsync(input); + } + + [HttpPut] + [Route("phone/reset-password")] + public virtual async Task ResetPasswordAsync(PhoneResetPasswordDto input) + { + await AccountAppService.ResetPasswordAsync(input); } [HttpPost] - [Route("phone/verify")] - public virtual async Task VerifyPhoneNumberAsync(VerifyDto input) + [Route("phone/send-signin-code")] + public virtual async Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input) { - await AccountAppService.VerifyPhoneNumberAsync(input); + await AccountAppService.SendPhoneSigninCodeAsync(input); } - [HttpPut] - [Route("phone/reset-password")] - public virtual async Task ResetPasswordAsync(PasswordResetDto passwordReset) + [HttpPost] + [Route("phone/send-register-code")] + public virtual async Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input) + { + await AccountAppService.SendPhoneRegisterCodeAsync(input); + } + + [HttpPost] + [Route("phone/send-password-reset-code")] + public virtual async Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input) { - await AccountAppService.ResetPasswordAsync(passwordReset); + await AccountAppService.SendPhoneResetPasswordCodeAsync(input); } } } diff --git a/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN.Abp.AspNetCore.Mvc.Validation.csproj b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN.Abp.AspNetCore.Mvc.Validation.csproj new file mode 100644 index 000000000..c0868d9c2 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN.Abp.AspNetCore.Mvc.Validation.csproj @@ -0,0 +1,24 @@ + + + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpAspNetCoreMvcValidationModule.cs b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpAspNetCoreMvcValidationModule.cs new file mode 100644 index 000000000..7ebbfa1e9 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpAspNetCoreMvcValidationModule.cs @@ -0,0 +1,31 @@ +using System; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.Validation.Localization; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.AspNetCore.Mvc.Validation +{ + [Obsolete("用于测试模型绑定与验证相关的类,无需引用")] + [DependsOn( + typeof(AbpAspNetCoreMvcModule))] + public class AbpAspNetCoreMvcValidationModule : AbpModule + { + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields"); + }); + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpDataAnnotationsMetadataProvider.cs b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpDataAnnotationsMetadataProvider.cs new file mode 100644 index 000000000..d0cbb3501 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpDataAnnotationsMetadataProvider.cs @@ -0,0 +1,563 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; + + +namespace LINGYUN.Abp.AspNetCore.Mvc.Validation +{ + public class AbpDataAnnotationsMetadataProvider : + IBindingMetadataProvider, + IDisplayMetadataProvider, + IValidationMetadataProvider + { + // The [Nullable] attribute is synthesized by the compiler. It's best to just compare the type name. + private const string NullableAttributeFullTypeName = "System.Runtime.CompilerServices.NullableAttribute"; + private const string NullableFlagsFieldName = "NullableFlags"; + + private const string NullableContextAttributeFullName = "System.Runtime.CompilerServices.NullableContextAttribute"; + private const string NullableContextFlagsFieldName = "Flag"; + + private readonly IStringLocalizerFactory _stringLocalizerFactory; + private readonly MvcOptions _options; + private readonly MvcDataAnnotationsLocalizationOptions _localizationOptions; + + public AbpDataAnnotationsMetadataProvider( + MvcOptions options, + IOptions localizationOptions, + IStringLocalizerFactory stringLocalizerFactory) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (localizationOptions == null) + { + throw new ArgumentNullException(nameof(localizationOptions)); + } + + _options = options; + _localizationOptions = localizationOptions.Value; + _stringLocalizerFactory = stringLocalizerFactory; + } + + /// + public void CreateBindingMetadata(BindingMetadataProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var editableAttribute = context.Attributes.OfType().FirstOrDefault(); + if (editableAttribute != null) + { + context.BindingMetadata.IsReadOnly = !editableAttribute.AllowEdit; + } + } + + /// + public void CreateDisplayMetadata(DisplayMetadataProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var attributes = context.Attributes; + var dataTypeAttribute = attributes.OfType().FirstOrDefault(); + var displayAttribute = attributes.OfType().FirstOrDefault(); + var displayColumnAttribute = attributes.OfType().FirstOrDefault(); + var displayFormatAttribute = attributes.OfType().FirstOrDefault(); + var displayNameAttribute = attributes.OfType().FirstOrDefault(); + var hiddenInputAttribute = attributes.OfType().FirstOrDefault(); + var scaffoldColumnAttribute = attributes.OfType().FirstOrDefault(); + var uiHintAttribute = attributes.OfType().FirstOrDefault(); + + // Special case the [DisplayFormat] attribute hanging off an applied [DataType] attribute. This property is + // non-null for DataType.Currency, DataType.Date, DataType.Time, and potentially custom [DataType] + // subclasses. The DataType.Currency, DataType.Date, and DataType.Time [DisplayFormat] attributes have a + // non-null DataFormatString and the DataType.Date and DataType.Time [DisplayFormat] attributes have + // ApplyFormatInEditMode==true. + if (displayFormatAttribute == null && dataTypeAttribute != null) + { + displayFormatAttribute = dataTypeAttribute.DisplayFormat; + } + + var displayMetadata = context.DisplayMetadata; + + // ConvertEmptyStringToNull + if (displayFormatAttribute != null) + { + displayMetadata.ConvertEmptyStringToNull = displayFormatAttribute.ConvertEmptyStringToNull; + } + + // DataTypeName + if (dataTypeAttribute != null) + { + displayMetadata.DataTypeName = dataTypeAttribute.GetDataTypeName(); + } + else if (displayFormatAttribute != null && !displayFormatAttribute.HtmlEncode) + { + displayMetadata.DataTypeName = nameof(DataType.Html); + } + + var containerType = context.Key.ContainerType ?? context.Key.ModelType; + IStringLocalizer localizer = null; + if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null) + { + localizer = _localizationOptions.DataAnnotationLocalizerProvider(containerType, _stringLocalizerFactory); + } + + // Description + if (displayAttribute != null) + { + if (localizer != null && + !string.IsNullOrEmpty(displayAttribute.Description) && + displayAttribute.ResourceType == null) + { + displayMetadata.Description = () => localizer[displayAttribute.Description]; + } + else + { + displayMetadata.Description = () => displayAttribute.GetDescription(); + } + } + + // DisplayFormatString + if (displayFormatAttribute != null) + { + displayMetadata.DisplayFormatString = displayFormatAttribute.DataFormatString; + } + + // DisplayName + // DisplayAttribute has precedence over DisplayNameAttribute. + if (displayAttribute?.GetName() != null) + { + if (localizer != null && + !string.IsNullOrEmpty(displayAttribute.Name) && + displayAttribute.ResourceType == null) + { + var displayName = localizer[displayAttribute.Name]; + displayMetadata.DisplayName = () => displayName; + } + else + { + displayMetadata.DisplayName = () => displayAttribute.GetName(); + } + } + else if (displayNameAttribute != null) + { + if (localizer != null && + !string.IsNullOrEmpty(displayNameAttribute.DisplayName)) + { + displayMetadata.DisplayName = () => localizer[displayNameAttribute.DisplayName]; + } + else + { + displayMetadata.DisplayName = () => displayNameAttribute.DisplayName; + } + } + + // EditFormatString + if (displayFormatAttribute != null && displayFormatAttribute.ApplyFormatInEditMode) + { + displayMetadata.EditFormatString = displayFormatAttribute.DataFormatString; + } + + // IsEnum et cetera + var underlyingType = Nullable.GetUnderlyingType(context.Key.ModelType) ?? context.Key.ModelType; + var underlyingTypeInfo = underlyingType.GetTypeInfo(); + + if (underlyingTypeInfo.IsEnum) + { + // IsEnum + displayMetadata.IsEnum = true; + + // IsFlagsEnum + displayMetadata.IsFlagsEnum = underlyingTypeInfo.IsDefined(typeof(FlagsAttribute), inherit: false); + + // EnumDisplayNamesAndValues and EnumNamesAndValues + // + // Order EnumDisplayNamesAndValues by DisplayAttribute.Order, then by the order of Enum.GetNames(). + // That method orders by absolute value, then its behavior is undefined (but hopefully stable). + // Add to EnumNamesAndValues in same order but Dictionary does not guarantee order will be preserved. + + var groupedDisplayNamesAndValues = new List>(); + var namesAndValues = new Dictionary(); + + IStringLocalizer enumLocalizer = null; + if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null) + { + enumLocalizer = _localizationOptions.DataAnnotationLocalizerProvider(underlyingType, _stringLocalizerFactory); + } + + var enumFields = Enum.GetNames(underlyingType) + .Select(name => underlyingType.GetField(name)) + .OrderBy(field => field.GetCustomAttribute(inherit: false)?.GetOrder() ?? 1000); + + foreach (var field in enumFields) + { + var groupName = GetDisplayGroup(field); + var value = ((Enum)field.GetValue(obj: null)).ToString("d"); + + groupedDisplayNamesAndValues.Add(new KeyValuePair( + new EnumGroupAndName( + groupName, + () => GetDisplayName(field, enumLocalizer)), + value)); + namesAndValues.Add(field.Name, value); + } + + displayMetadata.EnumGroupedDisplayNamesAndValues = groupedDisplayNamesAndValues; + displayMetadata.EnumNamesAndValues = namesAndValues; + } + + // HasNonDefaultEditFormat + if (!string.IsNullOrEmpty(displayFormatAttribute?.DataFormatString) && + displayFormatAttribute?.ApplyFormatInEditMode == true) + { + // Have a non-empty EditFormatString based on [DisplayFormat] from our cache. + if (dataTypeAttribute == null) + { + // Attributes include no [DataType]; [DisplayFormat] was applied directly. + displayMetadata.HasNonDefaultEditFormat = true; + } + else if (dataTypeAttribute.DisplayFormat != displayFormatAttribute) + { + // Attributes include separate [DataType] and [DisplayFormat]; [DisplayFormat] provided override. + displayMetadata.HasNonDefaultEditFormat = true; + } + else if (dataTypeAttribute.GetType() != typeof(DataTypeAttribute)) + { + // Attributes include [DisplayFormat] copied from [DataType] and [DataType] was of a subclass. + // Assume the [DataType] constructor used the protected DisplayFormat setter to override its + // default. That is derived [DataType] provided override. + displayMetadata.HasNonDefaultEditFormat = true; + } + } + + // HideSurroundingHtml + if (hiddenInputAttribute != null) + { + displayMetadata.HideSurroundingHtml = !hiddenInputAttribute.DisplayValue; + } + + // HtmlEncode + if (displayFormatAttribute != null) + { + displayMetadata.HtmlEncode = displayFormatAttribute.HtmlEncode; + } + + // NullDisplayText + if (displayFormatAttribute != null) + { + displayMetadata.NullDisplayText = displayFormatAttribute.NullDisplayText; + } + + // Order + if (displayAttribute?.GetOrder() != null) + { + displayMetadata.Order = displayAttribute.GetOrder().Value; + } + + // Placeholder + if (displayAttribute != null) + { + if (localizer != null && + !string.IsNullOrEmpty(displayAttribute.Prompt) && + displayAttribute.ResourceType == null) + { + displayMetadata.Placeholder = () => localizer[displayAttribute.Prompt]; + } + else + { + displayMetadata.Placeholder = () => displayAttribute.GetPrompt(); + } + } + + // ShowForDisplay + if (scaffoldColumnAttribute != null) + { + displayMetadata.ShowForDisplay = scaffoldColumnAttribute.Scaffold; + } + + // ShowForEdit + if (scaffoldColumnAttribute != null) + { + displayMetadata.ShowForEdit = scaffoldColumnAttribute.Scaffold; + } + + // SimpleDisplayProperty + if (displayColumnAttribute != null) + { + displayMetadata.SimpleDisplayProperty = displayColumnAttribute.DisplayColumn; + } + + // TemplateHint + if (uiHintAttribute != null) + { + displayMetadata.TemplateHint = uiHintAttribute.UIHint; + } + else if (hiddenInputAttribute != null) + { + displayMetadata.TemplateHint = "HiddenInput"; + } + } + + /// + public void CreateValidationMetadata(ValidationMetadataProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Read interface .Count once rather than per iteration + var contextAttributes = context.Attributes; + var contextAttributesCount = contextAttributes.Count; + var attributes = new List(contextAttributesCount); + + for (var i = 0; i < contextAttributesCount; i++) + { + var attribute = contextAttributes[i]; + if (attribute is ValidationProviderAttribute validationProviderAttribute) + { + attributes.AddRange(validationProviderAttribute.GetValidationAttributes()); + } + else + { + attributes.Add(attribute); + } + } + + // RequiredAttribute marks a property as required by validation - this means that it + // must have a non-null value on the model during validation. + var requiredAttribute = attributes.OfType().FirstOrDefault(); + + // For non-nullable reference types, treat them as-if they had an implicit [Required]. + // This allows the developer to specify [Required] to customize the error message, so + // if they already have [Required] then there's no need for us to do this check. + if (!_options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes && + requiredAttribute == null && + !context.Key.ModelType.IsValueType && + context.Key.MetadataKind != ModelMetadataKind.Type) + { + var addInferredRequiredAttribute = false; + if (context.Key.MetadataKind == ModelMetadataKind.Type) + { + // Do nothing. + } + else if (context.Key.MetadataKind == ModelMetadataKind.Property) + { + var property = context.Key.PropertyInfo; + if (property is null) + { + // PropertyInfo was unavailable on ModelIdentity prior to 3.1. + // Making a cogent argument about the nullability of the property requires inspecting the declared type, + // since looking at the runtime type may result in false positives: https://github.com/dotnet/aspnetcore/issues/14812 + // The only way we could arrive here is if the ModelMetadata was constructed using the non-default provider. + // We'll cursorily examine the attributes on the property, but not the ContainerType to make a decision about it's nullability. + + if (HasNullableAttribute(context.PropertyAttributes, out var propertyHasNullableAttribute)) + { + addInferredRequiredAttribute = propertyHasNullableAttribute; + } + } + else + { + addInferredRequiredAttribute = IsNullableReferenceType( + property.DeclaringType, + member: null, + context.PropertyAttributes); + } + } + else if (context.Key.MetadataKind == ModelMetadataKind.Parameter) + { + addInferredRequiredAttribute = IsNullableReferenceType( + context.Key.ParameterInfo?.Member.ReflectedType, + context.Key.ParameterInfo.Member, + context.ParameterAttributes); + } + else + { + throw new InvalidOperationException("Unsupported ModelMetadataKind: " + context.Key.MetadataKind); + } + + if (addInferredRequiredAttribute) + { + // Since this behavior specifically relates to non-null-ness, we will use the non-default + // option to tolerate empty/whitespace strings. empty/whitespace INPUT will still result in + // a validation error by default because we convert empty/whitespace strings to null + // unless you say otherwise. + requiredAttribute = new RequiredAttribute() + { + AllowEmptyStrings = true, + }; + attributes.Add(requiredAttribute); + } + } + + if (requiredAttribute != null) + { + context.ValidationMetadata.IsRequired = true; + } + + foreach (var attribute in attributes.OfType()) + { + // If another provider has already added this attribute, do not repeat it. + // This will prevent attributes like RemoteAttribute (which implement ValidationAttribute and + // IClientModelValidator) to be added to the ValidationMetadata twice. + // This is to ensure we do not end up with duplication validation rules on the client side. + if (!context.ValidationMetadata.ValidatorMetadata.Contains(attribute)) + { + context.ValidationMetadata.ValidatorMetadata.Add(attribute); + } + } + } + + private static string GetDisplayName(FieldInfo field, IStringLocalizer stringLocalizer) + { + var display = field.GetCustomAttribute(inherit: false); + if (display != null) + { + // Note [Display(Name = "")] is allowed but we will not attempt to localize the empty name. + var name = display.GetName(); + if (stringLocalizer != null && !string.IsNullOrEmpty(name) && display.ResourceType == null) + { + name = stringLocalizer[name]; + } + + return name ?? field.Name; + } + + return field.Name; + } + + // Return non-empty group specified in a [Display] attribute for a field, if any; string.Empty otherwise. + private static string GetDisplayGroup(FieldInfo field) + { + var display = field.GetCustomAttribute(inherit: false); + if (display != null) + { + // Note [Display(Group = "")] is allowed. + var group = display.GetGroupName(); + if (group != null) + { + return group; + } + } + + return string.Empty; + } + + internal static bool IsNullableReferenceType(Type containingType, MemberInfo member, IEnumerable attributes) + { + if (HasNullableAttribute(attributes, out var result)) + { + return result; + } + + return IsNullableBasedOnContext(containingType, member); + } + + // Internal for testing + internal static bool HasNullableAttribute(IEnumerable attributes, out bool isNullable) + { + // [Nullable] is compiler synthesized, comparing by name. + var nullableAttribute = attributes + .FirstOrDefault(a => string.Equals(a.GetType().FullName, NullableAttributeFullTypeName, StringComparison.Ordinal)); + if (nullableAttribute == null) + { + isNullable = false; + return false; // [Nullable] not found + } + + // We don't handle cases where generics and NNRT are used. This runs into a + // fundamental limitation of ModelMetadata - we use a single Type and Property/Parameter + // to look up the metadata. However when generics are involved and NNRT is in use + // the distance between the [Nullable] and member we're looking at is potentially + // unbounded. + // + // See: https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-reference-types.md#annotations + if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field && + field.GetValue(nullableAttribute) is byte[] flags && + flags.Length > 0 && + flags[0] == 1) // First element is the property/parameter type. + { + isNullable = true; + return true; // [Nullable] found and type is an NNRT + } + + isNullable = false; + return true; // [Nullable] found but type is not an NNRT + } + + internal static bool IsNullableBasedOnContext(Type containingType, MemberInfo member) + { + // For generic types, inspecting the nullability requirement additionally requires + // inspecting the nullability constraint on generic type parameters. This is fairly non-triviial + // so we'll just avoid calculating it. Users should still be able to apply an explicit [Required] + // attribute on these members. + if (containingType.IsGenericType) + { + return false; + } + + // The [Nullable] and [NullableContext] attributes are not inherited. + // + // The [NullableContext] attribute can appear on a method or on the module. + var attributes = member?.GetCustomAttributes(inherit: false) ?? Array.Empty(); + var isNullable = AttributesHasNullableContext(attributes); + if (isNullable != null) + { + return isNullable.Value; + } + + // Check on the containing type + var type = containingType; + do + { + attributes = type.GetCustomAttributes(inherit: false); + isNullable = AttributesHasNullableContext(attributes); + if (isNullable != null) + { + return isNullable.Value; + } + + type = type.DeclaringType; + } + while (type != null); + + // If we don't find the attribute on the declaring type then repeat at the module level + attributes = containingType.Module.GetCustomAttributes(inherit: false); + isNullable = AttributesHasNullableContext(attributes); + return isNullable ?? false; + + bool? AttributesHasNullableContext(object[] attributes) + { + var nullableContextAttribute = attributes + .FirstOrDefault(a => string.Equals(a.GetType().FullName, NullableContextAttributeFullName, StringComparison.Ordinal)); + if (nullableContextAttribute != null) + { + if (nullableContextAttribute.GetType().GetField(NullableContextFlagsFieldName) is FieldInfo field && + field.GetValue(nullableContextAttribute) is byte @byte) + { + return @byte == 1; // [NullableContext] found + } + } + + return null; + } + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpLocalizerModelMetadataProvider.cs b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpLocalizerModelMetadataProvider.cs new file mode 100644 index 000000000..a93fbb841 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/AbpLocalizerModelMetadataProvider.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using Volo.Abp.AspNetCore.Mvc.Validation; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Validation.Localization; + +namespace LINGYUN.Abp.AspNetCore.Mvc.Validation +{ + //[Dependency(ServiceLifetime.Singleton, ReplaceServices = true)] + //[ExposeServices(typeof(IModelMetadataProvider))] + public class AbpLocalizerModelMetadataProvider : DefaultModelMetadataProvider + { + protected IStringLocalizer StringLocalizer { get; } + public AbpLocalizerModelMetadataProvider( + ICompositeMetadataDetailsProvider detailsProvider, + IStringLocalizer stringLocalizer) + : base(detailsProvider) + { + StringLocalizer = stringLocalizer; + } + + public AbpLocalizerModelMetadataProvider( + IStringLocalizer stringLocalizer, + ICompositeMetadataDetailsProvider detailsProvider, + IOptions optionsAccessor) + : base(detailsProvider, optionsAccessor) + { + StringLocalizer = stringLocalizer; + } + + protected override DefaultMetadataDetails[] CreatePropertyDetails(ModelMetadataIdentity key) + { + var details = base.CreatePropertyDetails(key); + + foreach (var detail in details) + { + NormalizeMetadataDetail(detail); + } + + return details; + } + + private void NormalizeMetadataDetail(DefaultMetadataDetails detail) + { + foreach (var validationAttribute in detail.ModelAttributes.Attributes.OfType()) + { + NormalizeValidationAttrbute(validationAttribute, detail.DisplayMetadata?.DisplayFormatString ?? detail.Key.Name); + } + } + + protected virtual void NormalizeValidationAttrbute(ValidationAttribute validationAttribute, string displayName) + { + if (validationAttribute.ErrorMessage == null) + { + if (validationAttribute is RequiredAttribute required) + { + validationAttribute.ErrorMessage = StringLocalizer["The field {0} is invalid.", displayName]; + } + } + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/DataAnnotationAutoLocalizationMetadataDetailsProvider.cs b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/DataAnnotationAutoLocalizationMetadataDetailsProvider.cs new file mode 100644 index 000000000..d2361c67a --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/DataAnnotationAutoLocalizationMetadataDetailsProvider.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Mvc.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace LINGYUN.Abp.AspNetCore.Mvc.Validation +{ + public class DataAnnotationAutoLocalizationMetadataDetailsProvider : IDisplayMetadataProvider + { + private const string PropertyLocalizationKeyPrefix = "DisplayName:"; + + private readonly Lazy _stringLocalizerFactory; + private readonly Lazy> _localizationOptions; + + public DataAnnotationAutoLocalizationMetadataDetailsProvider(IServiceCollection services) + { + _stringLocalizerFactory = services.GetRequiredServiceLazy(); + _localizationOptions = services.GetRequiredServiceLazy>(); + } + + public void CreateDisplayMetadata(DisplayMetadataProviderContext context) + { + var displayMetadata = context.DisplayMetadata; + if (displayMetadata.DisplayName != null) + { + var displayName = displayMetadata.DisplayName(); + return; + } + + var attributes = context.Attributes; + + if (attributes.OfType().Any() || + attributes.OfType().Any()) + { + return; + } + + if (context.Key.Name.IsNullOrWhiteSpace()) + { + return; + } + + if (_localizationOptions.Value.Value.DataAnnotationLocalizerProvider == null) + { + return; + } + + var containerType = context.Key.ContainerType ?? context.Key.ModelType; + var localizer = _localizationOptions.Value.Value.DataAnnotationLocalizerProvider(containerType, _stringLocalizerFactory.Value); + + displayMetadata.DisplayName = () => + { + var localizedString = localizer[PropertyLocalizationKeyPrefix + context.Key.Name]; + + if (localizedString.ResourceNotFound) + { + localizedString = localizer[context.Key.Name]; + } + + return localizedString; + }; + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields/en.json b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields/en.json new file mode 100644 index 000000000..06b117b42 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "ThisFieldMustBeANumber": "The field must be a number!", + "The field {0} must be a number.": "The supplied value {0} must be a number!" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields/zh-Hans.json b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields/zh-Hans.json new file mode 100644 index 000000000..3abce0164 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.AspNetCore.Mvc.Validation/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "culture": "zh-Hans", + "texts": { + "ThisFieldMustBeANumber": "字段必须是数字!", + "The field {0} must be a number.": "提供的值 {0} 必须是数字!" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangeEmailAddressInput.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangeEmailAddressDto.cs similarity index 75% rename from aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangeEmailAddressInput.cs rename to aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangeEmailAddressDto.cs index 10b873607..9422247c7 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangeEmailAddressInput.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangeEmailAddressDto.cs @@ -5,20 +5,23 @@ using Volo.Abp.Validation; namespace LINGYUN.Abp.Identity { - public class ChangeEmailAddressInput + public class ChangeEmailAddressDto { /// /// 新邮件地址 /// [Required] [EmailAddress] + [Display(Name = "EmailAddress")] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] public string NewEmailAddress { get; set; } /// /// 安全验证码 /// + [Required] [DisableAuditing] - [StringLength(6)] + [StringLength(6, MinimumLength = 6)] + [Display(Name = "EmailVerifyCode")] public string Code { get; set; } } } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangePhoneNumberInput.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangePhoneNumberDto.cs similarity index 75% rename from aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangePhoneNumberInput.cs rename to aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangePhoneNumberDto.cs index 4d432971c..363e2c96c 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangePhoneNumberInput.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangePhoneNumberDto.cs @@ -5,7 +5,7 @@ using Volo.Abp.Validation; namespace LINGYUN.Abp.Identity { - public class ChangePhoneNumberInput + public class ChangePhoneNumberDto { /// /// 新手机号 @@ -13,12 +13,15 @@ namespace LINGYUN.Abp.Identity [Required] [Phone] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] + [Display(Name = "PhoneNumber")] public string NewPhoneNumber { get; set; } /// /// 安全验证码 /// + [Required] [DisableAuditing] - [StringLength(6)] + [StringLength(6, MinimumLength = 6)] + [Display(Name = "SmsVerifyCode")] public string Code { get; set; } } } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/IdentityUserTwoFactorEnabledDto.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangeTwoFactorEnabledDto.cs similarity index 65% rename from aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/IdentityUserTwoFactorEnabledDto.cs rename to aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangeTwoFactorEnabledDto.cs index 0450be9f6..21091277b 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/IdentityUserTwoFactorEnabledDto.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/ChangeTwoFactorEnabledDto.cs @@ -1,6 +1,6 @@ namespace LINGYUN.Abp.Identity { - public class IdentityUserTwoFactorEnabledDto + public class ChangeTwoFactorEnabledDto { public bool Enabled { get; set; } } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/SendChangeEmailAddressCodeDto.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/SendChangeEmailAddressCodeDto.cs new file mode 100644 index 000000000..a79222f6d --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/SendChangeEmailAddressCodeDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Identity; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.Identity +{ + public class SendChangeEmailAddressCodeDto + { + /// + /// 新邮件地址 + /// + [Required] + [EmailAddress] + [Display(Name = "EmailAddress")] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] + public string NewEmailAddress { get; set; } + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/SendChangePhoneNumberCodeDto.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/SendChangePhoneNumberCodeDto.cs new file mode 100644 index 000000000..f8a394e84 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/SendChangePhoneNumberCodeDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Identity; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.Identity +{ + public class SendChangePhoneNumberCodeDto + { + /// + /// 新手机号 + /// + [Required] + [Phone] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] + [Display(Name = "PhoneNumber")] + public string NewPhoneNumber { get; set; } + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IIdentityUserAppService.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IIdentityUserAppService.cs index 204f10fab..a248f657e 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IIdentityUserAppService.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IIdentityUserAppService.cs @@ -37,7 +37,7 @@ namespace LINGYUN.Abp.Identity /// /// /// - Task ChangeTwoFactorEnabledAsync(Guid id, IdentityUserTwoFactorEnabledDto input); + Task ChangeTwoFactorEnabledAsync(Guid id, ChangeTwoFactorEnabledDto input); /// /// 变更用户密码 /// diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IMyProfileAppService.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IMyProfileAppService.cs index f600f2a64..16f5c65ed 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IMyProfileAppService.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IMyProfileAppService.cs @@ -6,10 +6,25 @@ namespace LINGYUN.Abp.Identity public interface IMyProfileAppService : IApplicationService { /// - /// 变更双因素验证 + /// 改变二次认证 /// /// /// - Task ChangeTwoFactorEnabledAsync(IdentityUserTwoFactorEnabledDto input); + Task ChangeTwoFactorEnabledAsync(ChangeTwoFactorEnabledDto input); + /// + /// 发送改变手机号验证码 + /// + /// + /// + Task SendChangePhoneNumberCodeAsync(SendChangePhoneNumberCodeDto input); + /// + /// 改变手机绑定 + /// + /// + /// + /// + /// 需二次认证,主要是为了无法用到重定向页面修改相关信息的地方(点名微信小程序) + /// + Task ChangePhoneNumberAsync(ChangePhoneNumberDto input); } } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application/LINGYUN/Abp/Identity/IdentityUserAppService.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application/LINGYUN/Abp/Identity/IdentityUserAppService.cs index 2c69f05e6..6554b7577 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application/LINGYUN/Abp/Identity/IdentityUserAppService.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application/LINGYUN/Abp/Identity/IdentityUserAppService.cs @@ -124,7 +124,7 @@ namespace LINGYUN.Abp.Identity } [Authorize(Volo.Abp.Identity.IdentityPermissions.Users.Update)] - public virtual async Task ChangeTwoFactorEnabledAsync(Guid id, IdentityUserTwoFactorEnabledDto input) + public virtual async Task ChangeTwoFactorEnabledAsync(Guid id, ChangeTwoFactorEnabledDto input) { var user = await UserManager.GetByIdAsync(id); diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application/LINGYUN/Abp/Identity/MyProfileAppService.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application/LINGYUN/Abp/Identity/MyProfileAppService.cs index 41f8fefea..2b02cb316 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application/LINGYUN/Abp/Identity/MyProfileAppService.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application/LINGYUN/Abp/Identity/MyProfileAppService.cs @@ -1,7 +1,13 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Distributed; +using System; using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Caching; using Volo.Abp.Identity; +using Volo.Abp.Identity.Settings; +using Volo.Abp.Settings; using Volo.Abp.Users; namespace LINGYUN.Abp.Identity @@ -9,24 +15,83 @@ namespace LINGYUN.Abp.Identity [Authorize] public class MyProfileAppService : IdentityAppServiceBase, IMyProfileAppService { + protected IDistributedCache SecurityTokenCache { get; } + protected IUserSecurityCodeSender SecurityCodeSender { get; } protected IdentityUserManager UserManager { get; } protected IIdentityUserRepository UserRepository { get; } public MyProfileAppService( IdentityUserManager userManager, - IIdentityUserRepository userRepository) + IIdentityUserRepository userRepository, + IUserSecurityCodeSender securityCodeSender, + IDistributedCache securityTokenCache) { UserManager = userManager; UserRepository = userRepository; + SecurityCodeSender = securityCodeSender; + SecurityTokenCache = SecurityTokenCache; } - public virtual async Task ChangeTwoFactorEnabledAsync(IdentityUserTwoFactorEnabledDto input) + public virtual async Task ChangeTwoFactorEnabledAsync(ChangeTwoFactorEnabledDto input) { + if (!await SettingProvider.IsTrueAsync(IdentitySettingNames.TwoFactor.UsersCanChange)) + { + throw new BusinessException(Volo.Abp.Identity.IdentityErrorCodes.CanNotChangeTwoFactor); + } + var user = await UserManager.GetByIdAsync(CurrentUser.GetId()); (await UserManager.SetTwoFactorEnabledWithAccountConfirmedAsync(user, input.Enabled)).CheckErrors(); await CurrentUnitOfWork.SaveChangesAsync(); } + + public virtual async Task SendChangePhoneNumberCodeAsync(SendChangePhoneNumberCodeDto input) + { + var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.NewPhoneNumber, "SmsChangePhoneNumber"); + var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); + var interval = await SettingProvider.GetAsync(Settings.IdentitySettingNames.User.SmsRepetInterval, 1); + if (securityTokenCacheItem != null) + { + throw new UserFriendlyException(L["SendRepeatPhoneVerifyCode", interval]); + } + + // 是否已有用户使用手机号绑定 + if (await UserRepository.IsPhoneNumberConfirmedAsync(input.NewPhoneNumber)) + { + throw new BusinessException(IdentityErrorCodes.DuplicatePhoneNumber); + } + var user = await UserManager.GetByIdAsync(CurrentUser.GetId()); + var template = await SettingProvider.GetOrNullAsync(Settings.IdentitySettingNames.User.SmsPhoneNumberConfirmed); + var token = await UserManager.GenerateChangePhoneNumberTokenAsync(user, input.NewPhoneNumber); + // 发送验证码 + await SecurityCodeSender.SendPhoneConfirmedCodeAsync(input.NewPhoneNumber, token, template); + + securityTokenCacheItem = new SmsSecurityTokenCacheItem(token, user.ConcurrencyStamp); + await SecurityTokenCache + .SetAsync(securityTokenCacheKey, securityTokenCacheItem, + new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(interval) + }); + } + + public virtual async Task ChangePhoneNumberAsync(ChangePhoneNumberDto input) + { + // 是否已有用户使用手机号绑定 + if (await UserRepository.IsPhoneNumberConfirmedAsync(input.NewPhoneNumber)) + { + throw new BusinessException(IdentityErrorCodes.DuplicatePhoneNumber); + } + //TODO: 可以查询缓存用 securityTokenCacheItem.SecurityToken 与 user.SecurityStamp 作对比 + var user = await UserManager.GetByIdAsync(CurrentUser.GetId()); + // 更换手机号 + (await UserManager.ChangePhoneNumberAsync(user, input.NewPhoneNumber, input.Code)).CheckErrors(); + + await CurrentUnitOfWork.SaveChangesAsync(); + + var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.NewPhoneNumber, "SmsChangePhoneNumber"); + await SecurityTokenCache.RemoveAsync(securityTokenCacheKey); + } } } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IUserSecurityCodeSender.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IUserSecurityCodeSender.cs new file mode 100644 index 000000000..7ec4aeacb --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IUserSecurityCodeSender.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.Identity +{ + public interface IUserSecurityCodeSender + { + Task SendPhoneConfirmedCodeAsync( + string phone, + string token, + string template, // 传递模板号 + CancellationToken cancellation = default); + + Task SendEmailConfirmedCodeAsync( + string userName, + string email, + string token, + CancellationToken cancellation = default); + } +} 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 b79a7c77e..9af0aee7b 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 @@ -14,5 +14,13 @@ /// 手机号码已被使用 /// public const string DuplicatePhoneNumber = "Volo.Abp.Identity:020007"; + /// + /// 你不能修改你的手机绑定信息 + /// + public const string UsersCanNotChangePhoneNumber = "Volo.Abp.Identity:020008"; + /// + /// 你不能修改你的邮件绑定信息 + /// + public const string UsersCanNotChangeEmailAddress = "Volo.Abp.Identity:020009"; } } 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 f74341346..b43620235 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 @@ -14,6 +14,7 @@ "OrganizationUnit:AddChildren": "Add Children", "OrganizationUnit:AddMember": "Add Member", "OrganizationUnit:AddRole": "Add Role", + "OrganizationUnit:DisplayName": "Display Name", "OrganizationUnit:WillDelete": "Organization: {0} will be deleted", "OrganizationUnit:AreYouSureRemoveUser": "Are you sure you want to delete user {0} from your organization?", "OrganizationUnit:AreYouSureRemoveRole": "Are you sure you want to remove the role {0} from the organization?", @@ -41,6 +42,23 @@ "IdentityClaim:ValueType": "Value type", "Volo.Abp.Identity:020005": "The static claim type cannot be changed!", "Volo.Abp.Identity:020006": "Unable to delete static claim type!", - "Volo.Abp.Identity:020007": "The phone number is already tied to another user!" + "Volo.Abp.Identity:020007": "The phone number is already tied to another user!", + "Volo.Abp.Identity:020008": "You can't modify your phone's binding information!", + "Volo.Abp.Identity:020009": "You cannot modify your email binding information!", + "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", + "DisplayName:Abp.Identity.User.SmsUserSignin": "Signin sms template", + "Description:Abp.Identity.User.SmsUserSignin": "When the user logs in, he/she should send the template number of the SMS verification code and fill in the template number of the corresponding cloud platform registration", + "DisplayName:Abp.Identity.User.SmsResetPassword": "Reset password sms template", + "Description:Abp.Identity.User.SmsResetPassword": "When the user resets the password, he/she sends the template number of SMS verification code and fills in the template number registered on the cloud platform", + "DisplayName:Abp.Identity.User.SmsPhoneNumberConfirmed": "Phone number confirmation template", + "Description:Abp.Identity.User.SmsPhoneNumberConfirmed": "The user confirms the mobile phone verification code template", + "DisplayName:Abp.Identity.User.SmsRepetInterval": "SMS verification code validity(min)", + "Description:Abp.Identity.User.SmsRepetInterval": "The valid time for the user to send SMS verification code, unit m, default 3min", + "DisplayName:SmsVerifyCode": "SMS verification code", + "DisplayName:EmailVerifyCode": "Mail verification code", + "DisplayName:WeChatCode": "Wechat login code", + "SendRepeatSmsVerifyCode": "Phone verification code cannot be sent repeatedly within {0} minutes!" } } \ No newline at end of file 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 86bffcc92..549de3a63 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 @@ -42,6 +42,23 @@ "IdentityClaim:ValueType": "值类型", "Volo.Abp.Identity:020005": "无法变更静态声明类型!", "Volo.Abp.Identity:020006": "无法删除静态声明类型!", - "Volo.Abp.Identity:020007": "手机号码已被其他用户绑定!" + "Volo.Abp.Identity:020007": "手机号码已被其他用户绑定!", + "Volo.Abp.Identity:020008": "你不能修改你的手机绑定信息!", + "Volo.Abp.Identity:020009": "你不能修改你的邮件绑定信息!", + "Volo.Abp.Identity:DuplicatePhoneNumber": "手机号 '{0}' 已存在.", + "DisplayName:Abp.Identity.User.SmsNewUserRegister": "新用户注册模板", + "Description:Abp.Identity.User.SmsNewUserRegister": "新用户通过手机注册账号验证码模板", + "DisplayName:Abp.Identity.User.SmsUserSignin": "用户登录模板", + "Description:Abp.Identity.User.SmsUserSignin": "用户通过手机登录验证码模板", + "DisplayName:Abp.Identity.User.SmsResetPassword": "密码找回模板", + "Description:Abp.Identity.User.SmsResetPassword": "用户通过手机找回密码验证码模板", + "DisplayName:Abp.Identity.User.SmsPhoneNumberConfirmed": "手机确认模板", + "Description:Abp.Identity.User.SmsPhoneNumberConfirmed": "用户确认手机号验证码模板", + "DisplayName:Abp.Identity.User.SmsRepetInterval": "重复发送间隔时间(min)", + "Description:Abp.Identity.User.SmsRepetInterval": "验证码重复发送的最小时间差,单位为分", + "DisplayName:SmsVerifyCode": "短信验证码", + "DisplayName:EmailVerifyCode": "邮件验证码", + "DisplayName:WeChatCode": "微信登录凭证", + "SendRepeatSmsVerifyCode": "手机验证码不能在 {0} 分钟内重复发送!" } } \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Security/DefaultTotpService.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Security/DefaultTotpService.cs new file mode 100644 index 000000000..3892cc35c --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Security/DefaultTotpService.cs @@ -0,0 +1,121 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.Identity.Security +{ + /// + /// 微软的实现 + /// See: Microsoft.AspNetCore.Identity.Rfc6238AuthenticationService + /// + internal class DefaultTotpService : ITotpService, ISingletonDependency + { + private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3); + private static readonly Encoding _encoding = new UTF8Encoding(false, true); +#if NETSTANDARD2_0 + private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); +#endif + + // Generates a new 80-bit security token + public static byte[] GenerateRandomKey() + { + byte[] bytes = new byte[20]; +#if NETSTANDARD2_0 + _rng.GetBytes(bytes); +#else + RandomNumberGenerator.Fill(bytes); +#endif + return bytes; + } + + internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier) + { + // # of 0's = length of pin + const int Mod = 1000000; + + // See https://tools.ietf.org/html/rfc4226 + // We can add an optional modifier + var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber)); + var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier)); + + // Generate DT string + var offset = hash[hash.Length - 1] & 0xf; + Debug.Assert(offset + 4 < hash.Length); + var binaryCode = (hash[offset] & 0x7f) << 24 + | (hash[offset + 1] & 0xff) << 16 + | (hash[offset + 2] & 0xff) << 8 + | (hash[offset + 3] & 0xff); + + return binaryCode % Mod; + } + + private static byte[] ApplyModifier(byte[] input, string modifier) + { + if (String.IsNullOrEmpty(modifier)) + { + return input; + } + + var modifierBytes = _encoding.GetBytes(modifier); + var combined = new byte[checked(input.Length + modifierBytes.Length)]; + Buffer.BlockCopy(input, 0, combined, 0, input.Length); + Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); + return combined; + } + + // More info: https://tools.ietf.org/html/rfc6238#section-4 + private static ulong GetCurrentTimeStepNumber() + { +#if NETSTANDARD2_0 + var delta = DateTime.UtcNow - _unixEpoch; +#else + var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch; +#endif + return (ulong)(delta.Ticks / _timestep.Ticks); + } + + public int GenerateCode(byte[] securityToken, string modifier = null) + { + if (securityToken == null) + { + throw new ArgumentNullException(nameof(securityToken)); + } + + // Allow a variance of no greater than 9 minutes in either direction + var currentTimeStep = GetCurrentTimeStepNumber(); + using (var hashAlgorithm = new HMACSHA1(securityToken)) + { + return ComputeTotp(hashAlgorithm, currentTimeStep, modifier); + } + } + + public bool ValidateCode(byte[] securityToken, int code, string modifier = null) + { + if (securityToken == null) + { + throw new ArgumentNullException(nameof(securityToken)); + } + + // Allow a variance of no greater than 9 minutes in either direction + var currentTimeStep = GetCurrentTimeStepNumber(); + using (var hashAlgorithm = new HMACSHA1(securityToken)) + { + for (var i = -2; i <= 2; i++) + { + var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier); + if (computedTotp == code) + { + return true; + } + } + } + + // No match + return false; + } + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Security/ITotpService.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Security/ITotpService.cs new file mode 100644 index 000000000..557edc7bc --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Security/ITotpService.cs @@ -0,0 +1,12 @@ +namespace LINGYUN.Abp.Identity.Security +{ + /// + /// totp算法服务 + /// + public interface ITotpService + { + int GenerateCode(byte[] securityToken, string modifier = null); + + bool ValidateCode(byte[] securityToken, int code, string modifier = null); + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Settings/IdentitySettingDefinitionProvider.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Settings/IdentitySettingDefinitionProvider.cs index 5951a8e07..98b7aff87 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Settings/IdentitySettingDefinitionProvider.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Settings/IdentitySettingDefinitionProvider.cs @@ -8,6 +8,58 @@ namespace LINGYUN.Abp.Identity.Settings { public override void Define(ISettingDefinitionContext context) { + context.Add( + new SettingDefinition( + name: IdentitySettingNames.User.SmsNewUserRegister, + defaultValue: "", + displayName: L("DisplayName:Abp.Identity.User.SmsNewUserRegister"), + description: L("Description:Abp.Identity.User.SmsNewUserRegister"), + isVisibleToClients: true) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + name: IdentitySettingNames.User.SmsUserSignin, + defaultValue: "", + displayName: L("DisplayName:Abp.Identity.User.SmsUserSignin"), + description: L("Description:Abp.Identity.User.SmsUserSignin"), + isVisibleToClients: true) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + name: IdentitySettingNames.User.SmsResetPassword, + defaultValue: "", + displayName: L("DisplayName:Abp.Identity.User.SmsResetPassword"), + description: L("Description:Abp.Identity.User.SmsResetPassword"), + isVisibleToClients: true) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + name: IdentitySettingNames.User.SmsPhoneNumberConfirmed, + defaultValue: "", + displayName: L("DisplayName:Abp.Identity.User.SmsPhoneNumberConfirmed"), + description: L("Description:Abp.Identity.User.SmsPhoneNumberConfirmed"), + isVisibleToClients: true) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + name: IdentitySettingNames.User.SmsRepetInterval, + defaultValue: "5", + displayName: L("DisplayName:Abp.Identity.User.SmsRepetInterval"), + description: L("Description:Abp.Identity.User.SmsRepetInterval"), + isVisibleToClients: true) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName) + ); } private static LocalizableString L(string name) diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Settings/IdentitySettingNames.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Settings/IdentitySettingNames.cs index ee74f105d..ec7d9f94c 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Settings/IdentitySettingNames.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Settings/IdentitySettingNames.cs @@ -2,5 +2,30 @@ { public static class IdentitySettingNames { + private const string Prefix = "Abp.Identity"; + public static class User + { + private const string UserPrefix = Prefix + ".User"; + /// + /// 用户手机验证短信模板 + /// + public const string SmsPhoneNumberConfirmed = UserPrefix + ".SmsPhoneNumberConfirmed"; + /// + /// 用户注册短信验证码模板号 + /// + public const string SmsNewUserRegister = UserPrefix + ".SmsNewUserRegister"; + /// + /// 用户登录短信验证码模板号 + /// + public const string SmsUserSignin = UserPrefix + ".SmsUserSignin"; + /// + /// 用户重置密码短信验证码模板号 + /// + public const string SmsResetPassword = UserPrefix + ".SmsResetPassword"; + /// + /// 验证码重复间隔时间 + /// + public const string SmsRepetInterval = UserPrefix + ".SmsRepetInterval"; + } } } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IIdentityUserRepository.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IIdentityUserRepository.cs index 095ac5a75..ffe9f2db4 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IIdentityUserRepository.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/IIdentityUserRepository.cs @@ -8,21 +8,83 @@ namespace LINGYUN.Abp.Identity { public interface IIdentityUserRepository : Volo.Abp.Identity.IIdentityUserRepository { + /// + /// 手机号是否已被使用 + /// + /// + /// + /// + Task IsPhoneNumberUedAsync( + string phoneNumber, + CancellationToken cancellationToken = default); + /// + /// 手机号是否已确认(绑定) + /// + /// + /// + /// + Task IsPhoneNumberConfirmedAsync( + string phoneNumber, + CancellationToken cancellationToken = default); + /// + /// 邮件地址是否已确认(绑定) + /// + /// + /// + /// + Task IsNormalizedEmailConfirmedAsync( + string normalizedEmail, + CancellationToken cancellationToken = default); + /// + /// 通过手机号查询用户 + /// + /// 手机号码 + /// 是否已确认过 + /// + /// + /// + Task FindByPhoneNumberAsync( + string phoneNumber, + bool isConfirmed = true, + bool includeDetails = false, + CancellationToken cancellationToken = default); + /// + /// 通过用户主键列表获取用户 + /// + /// + /// + /// + /// Task> GetListByIdListAsync( List userIds, bool includeDetails = false, CancellationToken cancellationToken = default ); - + /// + /// 获取用户所有的组织机构列表 + /// + /// + /// + /// + /// + /// + /// + /// Task> GetOrganizationUnitsAsync( - Guid id, + Guid userId, string filter = null, bool includeDetails = false, int skipCount = 1, int maxResultCount = 10, CancellationToken cancellationToken = default ); - + /// + /// + /// + /// + /// + /// + /// Task GetUsersInOrganizationUnitCountAsync( Guid organizationUnitId, string filter = null, diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/SmsSecurityTokenCacheItem.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/SmsSecurityTokenCacheItem.cs new file mode 100644 index 000000000..1a866787d --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN/Abp/Identity/SmsSecurityTokenCacheItem.cs @@ -0,0 +1,39 @@ +namespace LINGYUN.Abp.Identity +{ + /// + /// 短信安全令牌验证缓存 + /// + public class SmsSecurityTokenCacheItem + { + /// + /// 用于验证的Token + /// + public string Token { get; set; } + /// + /// 用于验证的安全令牌 + /// + public string SecurityToken { get; set; } + + public SmsSecurityTokenCacheItem() + { + + } + + public SmsSecurityTokenCacheItem(string token, string securityToken) + { + Token = token; + SecurityToken = securityToken; + } + + /// + /// 生成查询Key + /// + /// 手机号 + /// 安全令牌用途 + /// + public static string CalculateCacheKey(string phoneNumber, string purpose) + { + return "Totp:" + purpose + ";p:" + phoneNumber; + } + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/PhoneNumberUserValidator.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/PhoneNumberUserValidator.cs new file mode 100644 index 000000000..823d4c828 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/PhoneNumberUserValidator.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; +using Volo.Abp.Identity.Localization; +using IIdentityUserRepository = LINGYUN.Abp.Identity.IIdentityUserRepository; + +namespace Microsoft.AspNetCore.Identity +{ + [Dependency(ServiceLifetime.Scoped, ReplaceServices = true)] + [ExposeServices(typeof(IUserValidator))] + public class PhoneNumberUserValidator : UserValidator + { + private readonly IStringLocalizer _stringLocalizer; + private readonly IIdentityUserRepository _userRepository; + + public PhoneNumberUserValidator( + IIdentityUserRepository userRepository, + IStringLocalizer stringLocalizer) + { + _userRepository = userRepository; + _stringLocalizer = stringLocalizer; + } + public override async Task ValidateAsync(UserManager manager, IdentityUser user) + { + var errors = new List(); + await ValidatePhoneNumberAsync(manager, user, errors); + + return (errors.Count > 0) + ? IdentityResult.Failed(errors.ToArray()) + : await base.ValidateAsync(manager, user); + } + + protected virtual async Task ValidatePhoneNumberAsync(UserManager manager, IdentityUser user, ICollection errors) + { + var phoneNumber = await manager.GetPhoneNumberAsync(user); + if (phoneNumber.IsNullOrWhiteSpace()) + { + return; + } + var findUser = await _userRepository.FindByPhoneNumberAsync(phoneNumber, false); + if (findUser != null && !findUser.Id.Equals(user.Id)) + { + //errors.Add(new IdentityError + //{ + // Code = "DuplicatePhoneNumber", + // Description = _stringLocalizer["DuplicatePhoneNumber", phoneNumber] + //}); + throw new UserFriendlyException(_stringLocalizer["Volo.Abp.Identity:DuplicatePhoneNumber", phoneNumber]); + } + } + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs index 1091f95c9..87ab1bec6 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs @@ -20,6 +20,44 @@ namespace LINGYUN.Abp.Identity.EntityFrameworkCore { } + public virtual async Task IsPhoneNumberUedAsync( + string phoneNumber, + CancellationToken cancellationToken = default) + { + return await DbSet.IncludeDetails(false) + .AnyAsync(user => user.PhoneNumber == phoneNumber, + GetCancellationToken(cancellationToken)); + } + + public virtual async Task IsPhoneNumberConfirmedAsync( + string phoneNumber, + CancellationToken cancellationToken = default) + { + return await DbSet.IncludeDetails(false) + .AnyAsync(user => user.PhoneNumber == phoneNumber && user.PhoneNumberConfirmed, + GetCancellationToken(cancellationToken)); + } + + public virtual async Task IsNormalizedEmailConfirmedAsync( + string normalizedEmail, + CancellationToken cancellationToken = default) + { + return await DbSet.IncludeDetails(false) + .AnyAsync(user => user.NormalizedEmail == normalizedEmail && user.EmailConfirmed, + GetCancellationToken(cancellationToken)); + } + + public virtual async Task FindByPhoneNumberAsync( + string phoneNumber, + bool isConfirmed = true, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await DbSet.IncludeDetails(includeDetails) + .Where(user => user.PhoneNumber == phoneNumber && user.PhoneNumberConfirmed == isConfirmed) + .FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); + } + public virtual async Task> GetListByIdListAsync( List userIds, bool includeDetails = false, diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/AbpIdentityHttpApiModule.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/AbpIdentityHttpApiModule.cs index 60f4e524c..a70740de7 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/AbpIdentityHttpApiModule.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/AbpIdentityHttpApiModule.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc.Localization; +using Volo.Abp.Identity.Localization; using Volo.Abp.Modularity; namespace LINGYUN.Abp.Identity @@ -10,6 +12,12 @@ namespace LINGYUN.Abp.Identity { public override void PreConfigureServices(ServiceConfigurationContext context) { + PreConfigure(options => + { + options.AddAssemblyResource(typeof(IdentityResource), typeof(AbpIdentityApplicationContractsModule).Assembly); + options.AddAssemblyResource(typeof(IdentityResource), typeof(Volo.Abp.Identity.AbpIdentityApplicationContractsModule).Assembly); + }); + PreConfigure(mvcBuilder => { mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpIdentityHttpApiModule).Assembly); diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/IdentityUserController.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/IdentityUserController.cs index 4b021748b..22280fc8f 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/IdentityUserController.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/IdentityUserController.cs @@ -87,7 +87,7 @@ namespace LINGYUN.Abp.Identity [HttpPut] [Route("change-two-factor")] - public virtual async Task ChangeTwoFactorEnabledAsync(Guid id, IdentityUserTwoFactorEnabledDto input) + public virtual async Task ChangeTwoFactorEnabledAsync(Guid id, ChangeTwoFactorEnabledDto input) { await UserAppService.ChangeTwoFactorEnabledAsync(id, input); } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/MyProfileController.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/MyProfileController.cs index 1ba29ec1b..0a8238426 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/MyProfileController.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.HttpApi/LINGYUN/Abp/Identity/MyProfileController.cs @@ -22,9 +22,23 @@ namespace LINGYUN.Abp.Identity [HttpPut] [Route("change-two-factor")] - public virtual async Task ChangeTwoFactorEnabledAsync(IdentityUserTwoFactorEnabledDto input) + public virtual async Task ChangeTwoFactorEnabledAsync(ChangeTwoFactorEnabledDto input) { await MyProfileAppService.ChangeTwoFactorEnabledAsync(input); } + + [HttpPut] + [Route("send-phone-number-change-code")] + public virtual async Task SendChangePhoneNumberCodeAsync(SendChangePhoneNumberCodeDto input) + { + await MyProfileAppService.SendChangePhoneNumberCodeAsync(input); + } + + [HttpPut] + [Route("change-phone-number")] + public virtual async Task ChangePhoneNumberAsync(ChangePhoneNumberDto input) + { + await MyProfileAppService.ChangePhoneNumberAsync(input); + } } } diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.HttpApi/LINGYUN/Abp/IdentityServer/AbpIdentityServerHttpApiModule.cs b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.HttpApi/LINGYUN/Abp/IdentityServer/AbpIdentityServerHttpApiModule.cs index 377557992..90e7c304d 100644 --- a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.HttpApi/LINGYUN/Abp/IdentityServer/AbpIdentityServerHttpApiModule.cs +++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.HttpApi/LINGYUN/Abp/IdentityServer/AbpIdentityServerHttpApiModule.cs @@ -1,6 +1,7 @@ using Localization.Resources.AbpUi; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.IdentityServer.Localization; using Volo.Abp.Localization; using Volo.Abp.Modularity; @@ -15,6 +16,11 @@ namespace LINGYUN.Abp.IdentityServer { public override void PreConfigureServices(ServiceConfigurationContext context) { + PreConfigure(options => + { + options.AddAssemblyResource(typeof(AbpIdentityServerResource), typeof(AbpIdentityServerApplicationContractsModule).Assembly); + }); + PreConfigure(mvcBuilder => { mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpIdentityServerHttpApiModule).Assembly); diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.SmsValidator/LINGYUN.Abp.IdentityServer.SmsValidator.csproj b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.SmsValidator/LINGYUN.Abp.IdentityServer.SmsValidator.csproj index d9dd7a591..21cde5ebb 100644 --- a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.SmsValidator/LINGYUN.Abp.IdentityServer.SmsValidator.csproj +++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.SmsValidator/LINGYUN.Abp.IdentityServer.SmsValidator.csproj @@ -22,7 +22,7 @@ - + diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.SmsValidator/LINGYUN/Abp/IdentityServer/SmsValidator/SmsTokenGrantValidator.cs b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.SmsValidator/LINGYUN/Abp/IdentityServer/SmsValidator/SmsTokenGrantValidator.cs index 95ddd848d..a5f265628 100644 --- a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.SmsValidator/LINGYUN/Abp/IdentityServer/SmsValidator/SmsTokenGrantValidator.cs +++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.SmsValidator/LINGYUN/Abp/IdentityServer/SmsValidator/SmsTokenGrantValidator.cs @@ -3,7 +3,7 @@ using IdentityServer4.Events; using IdentityServer4.Models; using IdentityServer4.Services; using IdentityServer4.Validation; -using LINGYUN.Abp.Account; +using LINGYUN.Abp.Identity; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; @@ -25,8 +25,6 @@ namespace LINGYUN.Abp.IdentityServer.SmsValidator protected UserManager UserManager { get; } protected SignInManager SignInManager { get; } protected IStringLocalizer Localizer { get; } - protected PhoneNumberTokenProvider PhoneNumberTokenProvider { get; } - public SmsTokenGrantValidator( IEventService eventService, @@ -34,7 +32,6 @@ namespace LINGYUN.Abp.IdentityServer.SmsValidator SignInManager signInManager, IIdentityUserRepository userRepository, IStringLocalizer stringLocalizer, - PhoneNumberTokenProvider phoneNumberTokenProvider, ILogger logger) { Logger = logger; @@ -43,7 +40,6 @@ namespace LINGYUN.Abp.IdentityServer.SmsValidator SignInManager = signInManager; Localizer = stringLocalizer; UserRepository = userRepository; - PhoneNumberTokenProvider = phoneNumberTokenProvider; } public string GrantType => SmsValidatorConsts.SmsValidatorGrantTypeName; @@ -76,7 +72,8 @@ namespace LINGYUN.Abp.IdentityServer.SmsValidator Localizer["InvalidGrant:PhoneNumberNotRegister"]); return; } - var validResult = await PhoneNumberTokenProvider.ValidateAsync(SmsValidatorConsts.SmsValidatorPurpose, phoneToken, UserManager, currentUser); + + var validResult = await UserManager.VerifyTwoFactorTokenAsync(currentUser, TokenOptions.DefaultPhoneProvider, phoneToken); if (!validResult) { Logger.LogWarning("Authentication failed for token: {0}, reason: invalid token", phoneToken); @@ -85,6 +82,7 @@ namespace LINGYUN.Abp.IdentityServer.SmsValidator await EventService.RaiseAsync(new UserLoginFailureEvent(currentUser.UserName, $"invalid phone verify code {phoneToken}", false)); return; } + var sub = await UserManager.GetUserIdAsync(currentUser); var additionalClaims = new List(); @@ -95,6 +93,9 @@ namespace LINGYUN.Abp.IdentityServer.SmsValidator await EventService.RaiseAsync(new UserLoginSuccessEvent(currentUser.UserName, phoneNumber, null)); context.Result = new GrantValidationResult(sub, OidcConstants.AuthenticationMethods.ConfirmationBySms, additionalClaims.ToArray()); + + // 登录之后需要更新安全令牌 + (await UserManager.UpdateSecurityStampAsync(currentUser)).CheckErrors(); } } } diff --git a/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs b/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs index e4950c4c3..600439192 100644 --- a/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs +++ b/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs @@ -1,7 +1,8 @@ using DotNetCore.CAP; -using LINGYUN.Abp.Account; using LINGYUN.Abp.EventBus.CAP; +using LINGYUN.Abp.Identity.EntityFrameworkCore; using LINGYUN.Abp.IdentityServer; +using LINGYUN.Abp.IdentityServer.EntityFrameworkCore; using LINGYUN.Abp.MultiTenancy.DbFinder; using LINGYUN.Abp.PermissionManagement.Identity; using LINYUN.Abp.Sms.Aliyun; @@ -35,8 +36,6 @@ using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Identity; -using Volo.Abp.Identity.EntityFrameworkCore; -using Volo.Abp.IdentityServer.EntityFrameworkCore; using Volo.Abp.IdentityServer.Jwt; using Volo.Abp.Localization; using Volo.Abp.Modularity; @@ -52,7 +51,6 @@ using Volo.Abp.VirtualFileSystem; namespace AuthServer.Host { [DependsOn( - typeof(AbpAccountDomainModule), typeof(AbpAccountWebIdentityServerModule), typeof(AbpAccountApplicationModule), typeof(AbpAspNetCoreMvcUiMultiTenancyModule), @@ -170,7 +168,7 @@ namespace AuthServer.Host { options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"]; // 邮件登录地址 - options.Applications["MVC"].Urls[LINGYUN.Abp.Account.AccountUrlNames.MailLoginVerify] = "Account/VerifyCode"; + options.Applications["MVC"].Urls["EmailVerifyLogin"] = "Account/VerifyCode"; }); context.Services.ConfigureNonBreakingSameSiteCookies(); diff --git a/aspnet-core/services/account/AuthServer.Host/AuthServer.Host.csproj b/aspnet-core/services/account/AuthServer.Host/AuthServer.Host.csproj index de3d68317..28b9b9d45 100644 --- a/aspnet-core/services/account/AuthServer.Host/AuthServer.Host.csproj +++ b/aspnet-core/services/account/AuthServer.Host/AuthServer.Host.csproj @@ -47,18 +47,17 @@ - - - + + diff --git a/aspnet-core/services/account/AuthServer.Host/EntityFrameworkCore/Identity/EfCoreIdentityUserExtensionRepository.cs b/aspnet-core/services/account/AuthServer.Host/EntityFrameworkCore/Identity/EfCoreIdentityUserExtensionRepository.cs deleted file mode 100644 index 69727933e..000000000 --- a/aspnet-core/services/account/AuthServer.Host/EntityFrameworkCore/Identity/EfCoreIdentityUserExtensionRepository.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.Identity; -using Volo.Abp.Identity.EntityFrameworkCore; - -namespace AuthServer.Host.EntityFrameworkCore.Identity -{ - public class EfCoreIdentityUserRepository : EfCoreRepository, LINGYUN.Abp.Account.IIdentityUserRepository, - ITransientDependency - { - public EfCoreIdentityUserRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public virtual async Task PhoneNumberHasRegistedAsync(string phoneNumber) - { - return await DbSet.AnyAsync(x => x.PhoneNumberConfirmed && x.PhoneNumber.Equals(phoneNumber)); - } - - public virtual async Task GetIdByPhoneNumberAsync(string phoneNumber) - { - return await DbSet - .Where(x => x.PhoneNumber.Equals(phoneNumber)) - .Select(x => x.Id) - .FirstOrDefaultAsync(); - } - - public virtual async Task FindByPhoneNumberAsync(string phoneNumber) - { - return await WithDetails() - .Where(usr => usr.PhoneNumber.Equals(phoneNumber)) - .AsNoTracking() - .FirstOrDefaultAsync(); - } - - public override IQueryable WithDetails() - { - return DbSet - .Include(x => x.Claims) - .Include(x => x.Roles) - .Include(x => x.Logins) - .Include(x => x.Tokens); - } - } -} diff --git a/aspnet-core/services/account/AuthServer.Host/Pages/Account/SendCode.cshtml.cs b/aspnet-core/services/account/AuthServer.Host/Pages/Account/SendCode.cshtml.cs index 4d878ee94..bcee0841c 100644 --- a/aspnet-core/services/account/AuthServer.Host/Pages/Account/SendCode.cshtml.cs +++ b/aspnet-core/services/account/AuthServer.Host/Pages/Account/SendCode.cshtml.cs @@ -1,4 +1,5 @@ using AuthServer.Host.Emailing; +using LINGYUN.Abp.Identity.Settings; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using System.Collections.Generic; @@ -100,8 +101,8 @@ namespace LINGYUN.Abp.Account.Web.Pages.Account else if (Input.SelectedProvider == "Phone") { var phoneNumber = await UserManager.GetPhoneNumberAsync(user); - var templateCode = await SettingProvider.GetOrNullAsync(AccountSettingNames.SmsSigninTemplateCode); - Check.NotNullOrWhiteSpace(templateCode, nameof(AccountSettingNames.SmsSigninTemplateCode)); + var templateCode = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsUserSignin); + Check.NotNullOrWhiteSpace(templateCode, nameof(IdentitySettingNames.User.SmsUserSignin)); // TODO: Ժչģ巢 var smsMessage = new SmsMessage(phoneNumber, code); diff --git a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/AbpIdentityServerAdminHttpApiHostModule.cs b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/AbpIdentityServerAdminHttpApiHostModule.cs index 75aa85d7e..42a04ffdf 100644 --- a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/AbpIdentityServerAdminHttpApiHostModule.cs +++ b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/AbpIdentityServerAdminHttpApiHostModule.cs @@ -1,5 +1,4 @@ using DotNetCore.CAP; -using IdentityModel; using LINGYUN.Abp.EventBus.CAP; using LINGYUN.Abp.ExceptionHandling; using LINGYUN.Abp.ExceptionHandling.Emailing; @@ -14,13 +13,11 @@ using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using StackExchange.Redis; using System; using System.Text; using Volo.Abp; -using Volo.Abp.Account; using Volo.Abp.AspNetCore.Authentication.JwtBearer; using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; using Volo.Abp.AspNetCore.Security.Claims; @@ -34,7 +31,6 @@ using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.Identity.Localization; -using Volo.Abp.IdentityServer.EntityFrameworkCore; using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; @@ -50,16 +46,14 @@ namespace LINGYUN.Abp.IdentityServer4 { [DependsOn( typeof(AbpAspNetCoreMvcUiMultiTenancyModule), - typeof(LINGYUN.Abp.Identity.AbpIdentityHttpApiModule), - typeof(LINGYUN.Abp.Identity.AbpIdentityApplicationModule), typeof(LINGYUN.Abp.Account.AbpAccountApplicationModule), typeof(LINGYUN.Abp.Account.AbpAccountHttpApiModule), + typeof(LINGYUN.Abp.Identity.AbpIdentityApplicationModule), + typeof(LINGYUN.Abp.Identity.AbpIdentityHttpApiModule), typeof(LINGYUN.Abp.IdentityServer.AbpIdentityServerApplicationModule), typeof(LINGYUN.Abp.IdentityServer.AbpIdentityServerHttpApiModule), typeof(LINGYUN.Abp.Identity.EntityFrameworkCore.AbpIdentityEntityFrameworkCoreModule), typeof(LINGYUN.Abp.IdentityServer.EntityFrameworkCore.AbpIdentityServerEntityFrameworkCoreModule), - typeof(AbpAccountApplicationModule), - typeof(AbpAccountHttpApiModule), typeof(AbpEntityFrameworkCoreMySQLModule), typeof(AbpAuditLoggingEntityFrameworkCoreModule), typeof(AbpTenantManagementEntityFrameworkCoreModule), @@ -180,7 +174,7 @@ namespace LINGYUN.Abp.IdentityServer4 // 滑动过期30天 options.GlobalCacheEntryOptions.SlidingExpiration = TimeSpan.FromDays(30); // 绝对过期60天 - options.GlobalCacheEntryOptions.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60); + options.GlobalCacheEntryOptions.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(60); }); Configure(options => diff --git a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/EmailConfirmed.tpl b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/EmailConfirmed.tpl new file mode 100644 index 000000000..157f51bb5 --- /dev/null +++ b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/EmailConfirmed.tpl @@ -0,0 +1,5 @@ +
+ {{L "EmailConfirmed" model.user}} +

{{model.code}}

+ {{L "EmailConfirmedRemarks"}} +
\ No newline at end of file diff --git a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/IdentityEmailTemplateDefinitionProvider.cs b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/IdentityEmailTemplateDefinitionProvider.cs new file mode 100644 index 000000000..4bda45557 --- /dev/null +++ b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/IdentityEmailTemplateDefinitionProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Emailing.Templates; +using Volo.Abp.Localization; +using Volo.Abp.TextTemplating; +using Volo.Abp.Identity.Localization; + +namespace LINGYUN.Abp.IdentityServer4.Emailing.Templates +{ + public class IdentityEmailTemplateDefinitionProvider : TemplateDefinitionProvider + { + public override void Define(ITemplateDefinitionContext context) + { + context.Add( + new TemplateDefinition( + IdentityEmailTemplates.EmailConfirmed, + displayName: LocalizableString.Create($"TextTemplate:{IdentityEmailTemplates.EmailConfirmed}"), + layout: StandardEmailTemplates.Layout, + localizationResource: typeof(IdentityResource) + ).WithVirtualFilePath("/LINGYUN/Abp/IdentityServer4/Emailing/Templates/EmailConfirmed.tpl", true) + ); + } + } +} diff --git a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/IdentityEmailTemplates.cs b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/IdentityEmailTemplates.cs new file mode 100644 index 000000000..3dab94dee --- /dev/null +++ b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Emailing/Templates/IdentityEmailTemplates.cs @@ -0,0 +1,7 @@ +namespace LINGYUN.Abp.IdentityServer4.Emailing.Templates +{ + public static class IdentityEmailTemplates + { + public const string EmailConfirmed = "Abp.Identity.EmailConfirmed"; + } +} diff --git a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/EntityFrameworkCore/Identity/EfCoreIdentityUserExtensionRepository.cs b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/EntityFrameworkCore/Identity/EfCoreIdentityUserExtensionRepository.cs deleted file mode 100644 index 99d394662..000000000 --- a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/EntityFrameworkCore/Identity/EfCoreIdentityUserExtensionRepository.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.Identity; -using Volo.Abp.Identity.EntityFrameworkCore; - -namespace LINGYUN.Abp.IdentityServer4.Identity -{ - public class EfCoreIdentityUserRepository : EfCoreRepository, Abp.Account.IIdentityUserRepository, - ITransientDependency - { - public EfCoreIdentityUserRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public virtual async Task PhoneNumberHasRegistedAsync(string phoneNumber) - { - return await DbSet.AnyAsync(x => x.PhoneNumberConfirmed && x.PhoneNumber.Equals(phoneNumber)); - } - - public virtual async Task GetIdByPhoneNumberAsync(string phoneNumber) - { - return await DbSet - .Where(x => x.PhoneNumber.Equals(phoneNumber)) - .Select(x => x.Id) - .FirstOrDefaultAsync(); - } - - public virtual async Task FindByPhoneNumberAsync(string phoneNumber) - { - return await DbSet.Where(usr => usr.PhoneNumber.Equals(phoneNumber)) - .AsNoTracking() - .FirstOrDefaultAsync(); - } - } -} diff --git a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/LINGYUN.Abp.IdentityServer4.HttpApi.Host.csproj b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/LINGYUN.Abp.IdentityServer4.HttpApi.Host.csproj index d7759f7c0..7b13bf676 100644 --- a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/LINGYUN.Abp.IdentityServer4.HttpApi.Host.csproj +++ b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/LINGYUN.Abp.IdentityServer4.HttpApi.Host.csproj @@ -11,6 +11,11 @@
+ + + + + @@ -61,4 +66,10 @@ + + + Always + + + \ No newline at end of file diff --git a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Properties/launchSettings.json b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Properties/launchSettings.json index 408992e95..1585148ef 100644 --- a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Properties/launchSettings.json +++ b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/Properties/launchSettings.json @@ -1,7 +1,7 @@ -{ +{ "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, + "windowsAuthentication": false, + "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:49612", "sslPort": 0 @@ -10,11 +10,11 @@ "profiles": { "LINGYUN.Abp.IdentityServer4.HttpApi.Host": { "commandName": "Project", - "launchBrowser": false, - "applicationUrl": "http://127.0.0.1:30015", "environmentVariables": { + "COMPlus_legacyCorruptedStateExceptionsPolicy": "1", "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "http://127.0.0.1:30015" } } -} +} \ No newline at end of file diff --git a/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/UserSecurityCodeSender.cs b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/UserSecurityCodeSender.cs new file mode 100644 index 000000000..7199a80f7 --- /dev/null +++ b/aspnet-core/services/identity-server/LINGYUN.Abp.IdentityServer4.HttpApi.Host/UserSecurityCodeSender.cs @@ -0,0 +1,66 @@ +using LINGYUN.Abp.Identity; +using LINGYUN.Abp.IdentityServer4.Emailing.Templates; +using Microsoft.Extensions.Localization; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Emailing; +using Volo.Abp.Identity.Localization; +using Volo.Abp.Sms; +using Volo.Abp.TextTemplating; + +namespace LINGYUN.Abp.IdentityServer4 +{ + public class UserSecurityCodeSender : IUserSecurityCodeSender, ITransientDependency + { + protected IEmailSender EmailSender { get; } + protected ITemplateRenderer TemplateRenderer { get; } + protected IStringLocalizer Localizer { get; } + + protected ISmsSender SmsSender { get; } + + public UserSecurityCodeSender( + ISmsSender smsSender, + IEmailSender emailSender, + ITemplateRenderer templateRenderer, + IStringLocalizer localizer) + { + SmsSender = smsSender; + EmailSender = emailSender; + TemplateRenderer = templateRenderer; + Localizer = localizer; + } + + public virtual async Task SendEmailConfirmedCodeAsync( + string userName, + string email, + string token, + CancellationToken cancellation = default) + { + var emailContent = await TemplateRenderer.RenderAsync( + IdentityEmailTemplates.EmailConfirmed, + new { user = userName, code = token }); + + await EmailSender.SendAsync( + email, + Localizer["EmailConfirmed"], + emailContent); + } + + public virtual async Task SendPhoneConfirmedCodeAsync( + string phone, + string token, + string template, + CancellationToken cancellation = default) + { + Check.NotNullOrWhiteSpace(template, nameof(template)); + + var smsMessage = new SmsMessage(phone, token); + smsMessage.Properties.Add("code", token); + smsMessage.Properties.Add("TemplateCode", template); + + await SmsSender.SendAsync(smsMessage); + } + } +} diff --git a/vueJs/src/api/users.ts b/vueJs/src/api/users.ts index 328e23d1b..5d329ffb0 100644 --- a/vueJs/src/api/users.ts +++ b/vueJs/src/api/users.ts @@ -169,13 +169,40 @@ export default class UserApiService { }) } - public static sendPhoneVerifyCode(phoneVerify: PhoneVerify) { - const _url = '/api/account/phone/verify' - return ApiService.HttpRequest({ + /** 发送短信登录验证码 */ + public static sendSmsSigninCode(phoneNumber: string) { + const _url = '/api/account/phone/send-signin-code' + return ApiService.HttpRequest({ baseURL: IdentityServiceUrl, url: _url, method: 'POST', - data: phoneVerify + data: { + phoneNumber: phoneNumber + } + }) + } + + public static sendSmsResetPasswordCode(phoneNumber: string) { + const _url = '/api/account/phone/send-password-reset-code' + return ApiService.HttpRequest({ + baseURL: IdentityServiceUrl, + url: _url, + method: 'POST', + data: { + phoneNumber: phoneNumber + } + }) + } + + public static sendSmsRegisterCode(phoneNumber: string) { + const _url = '/api/account/phone/send-register-code' + return ApiService.HttpRequest({ + baseURL: IdentityServiceUrl, + url: _url, + method: 'POST', + data: { + phoneNumber: phoneNumber + } }) } diff --git a/vueJs/src/views/login/index.vue b/vueJs/src/views/login/index.vue index 438015425..1a5b47043 100644 --- a/vueJs/src/views/login/index.vue +++ b/vueJs/src/views/login/index.vue @@ -157,7 +157,7 @@ import { Dictionary } from 'vue-router/types/router' import TenantBox from '@/components/TenantBox/index.vue' import LangSelect from '@/components/LangSelect/index.vue' import { Component, Vue, Watch } from 'vue-property-decorator' -import UserService, { PhoneVerify, VerifyType } from '@/api/users' +import UserService from '@/api/users' import { AbpModule } from '@/store/modules/abp' @Component({ @@ -300,10 +300,7 @@ export default class extends Vue { frmLogin.validateField('phoneNumber', (errorMsg: string) => { if (!errorMsg) { this.sending = true - const phoneVerify = new PhoneVerify() - phoneVerify.phoneNumber = this.loginForm.phoneNumber - phoneVerify.verifyType = VerifyType.Signin - UserService.sendPhoneVerifyCode(phoneVerify).then(() => { + UserService.sendSmsSigninCode(this.loginForm.phoneNumber).then(() => { let interValTime = 60 const sendingName = this.l('login.afterSendVerifyCode') const sendedName = this.l('login.sendVerifyCode') diff --git a/vueJs/src/views/register/index.vue b/vueJs/src/views/register/index.vue index fc6229b62..f031627f6 100644 --- a/vueJs/src/views/register/index.vue +++ b/vueJs/src/views/register/index.vue @@ -124,7 +124,7 @@ import { Dictionary } from 'vue-router/types/router' import TenantBox from '@/components/TenantBox/index.vue' import LangSelect from '@/components/LangSelect/index.vue' import { Component, Vue, Watch } from 'vue-property-decorator' -import UserService, { PhoneVerify, VerifyType, UserRegisterData } from '@/api/users' +import UserService, { UserRegisterData } from '@/api/users' import { AbpModule } from '@/store/modules/abp' @Component({ @@ -246,10 +246,7 @@ export default class extends Vue { frmLogin.validateField('phoneNumber', (errorMsg: string) => { if (!errorMsg) { this.sending = true - const phoneVerify = new PhoneVerify() - phoneVerify.phoneNumber = this.registerForm.phoneNumber - phoneVerify.verifyType = VerifyType.Register - UserService.sendPhoneVerifyCode(phoneVerify).then(() => { + UserService.sendSmsRegisterCode(this.registerForm.phoneNumber).then(() => { let interValTime = 60 const sendingName = this.l('login.afterSendVerifyCode') const sendedName = this.l('login.sendVerifyCode') diff --git a/vueJs/src/views/reset-password/index.vue b/vueJs/src/views/reset-password/index.vue index cd5030340..77e74949e 100644 --- a/vueJs/src/views/reset-password/index.vue +++ b/vueJs/src/views/reset-password/index.vue @@ -112,7 +112,7 @@ import { Dictionary } from 'vue-router/types/router' import TenantBox from '@/components/TenantBox/index.vue' import LangSelect from '@/components/LangSelect/index.vue' import { Component, Vue, Watch } from 'vue-property-decorator' -import UserService, { PhoneVerify, VerifyType, UserResetPasswordData } from '@/api/users' +import UserService, { UserResetPasswordData } from '@/api/users' import { AbpModule } from '@/store/modules/abp' @Component({ @@ -226,10 +226,7 @@ export default class extends Vue { frmResetPassword.validateField('phoneNumber', (errorMsg: string) => { if (!errorMsg) { this.sending = true - const phoneVerify = new PhoneVerify() - phoneVerify.phoneNumber = this.resetPasswordForm.phoneNumber - phoneVerify.verifyType = VerifyType.ResetPassword - UserService.sendPhoneVerifyCode(phoneVerify).then(() => { + UserService.sendSmsResetPasswordCode(this.resetPasswordForm.phoneNumber).then(() => { let interValTime = 60 const sendingName = this.l('login.afterSendVerifyCode') const sendedName = this.l('login.sendVerifyCode')