2 changed files with 320 additions and 320 deletions
@ -1,319 +1,319 @@ |
|||
using LINGYUN.Abp.Identity; |
|||
using LINGYUN.Abp.Identity.Security; |
|||
using LINGYUN.Abp.Identity.Settings; |
|||
using LINGYUN.Abp.WeChat; |
|||
using LINGYUN.Abp.WeChat.MiniProgram; |
|||
using LINGYUN.Abp.WeChat.OpenId; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Microsoft.Extensions.Options; |
|||
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.Validation; |
|||
using IIdentityUserRepository = LINGYUN.Abp.Identity.IIdentityUserRepository; |
|||
|
|||
namespace LINGYUN.Abp.Account |
|||
{ |
|||
public class AccountAppService : ApplicationService, IAccountAppService |
|||
{ |
|||
protected ITotpService TotpService { get; } |
|||
protected IdentityUserStore UserStore { get; } |
|||
protected IdentityUserManager UserManager { get; } |
|||
protected IIdentityUserRepository UserRepository { get; } |
|||
protected IUserSecurityCodeSender SecurityCodeSender { get; } |
|||
protected IWeChatOpenIdFinder WeChatOpenIdFinder { get; } |
|||
protected IOptions<IdentityOptions> IdentityOptions { get; } |
|||
protected AbpWeChatMiniProgramOptionsFactory MiniProgramOptionsFactory { get; } |
|||
protected IDistributedCache<SmsSecurityTokenCacheItem> SecurityTokenCache { get; } |
|||
|
|||
public AccountAppService( |
|||
ITotpService totpService, |
|||
IdentityUserStore userStore, |
|||
IdentityUserManager userManager, |
|||
IWeChatOpenIdFinder weChatOpenIdFinder, |
|||
IIdentityUserRepository userRepository, |
|||
IUserSecurityCodeSender securityCodeSender, |
|||
IDistributedCache<SmsSecurityTokenCacheItem> securityTokenCache, |
|||
AbpWeChatMiniProgramOptionsFactory miniProgramOptionsFactory, |
|||
IOptions<IdentityOptions> identityOptions) |
|||
{ |
|||
TotpService = totpService; |
|||
UserStore = userStore; |
|||
UserManager = userManager; |
|||
UserRepository = userRepository; |
|||
WeChatOpenIdFinder = weChatOpenIdFinder; |
|||
SecurityCodeSender = securityCodeSender; |
|||
SecurityTokenCache = securityTokenCache; |
|||
MiniProgramOptionsFactory = miniProgramOptionsFactory; |
|||
IdentityOptions = identityOptions; |
|||
|
|||
LocalizationResource = typeof(AccountResource); |
|||
} |
|||
|
|||
public virtual async Task RegisterAsync(WeChatRegisterDto input) |
|||
{ |
|||
ThowIfInvalidEmailAddress(input.EmailAddress); |
|||
|
|||
await CheckSelfRegistrationAsync(); |
|||
await IdentityOptions.SetAsync(); |
|||
|
|||
var options = await MiniProgramOptionsFactory.CreateAsync(); |
|||
|
|||
var wehchatOpenId = await WeChatOpenIdFinder.FindAsync(input.Code, options.AppId, options.AppSecret); |
|||
|
|||
var user = await UserManager.FindByLoginAsync(AbpWeChatMiniProgramConsts.ProviderKey, wehchatOpenId.OpenId); |
|||
if (user != null) |
|||
{ |
|||
// 应该要抛出微信号已注册异常,而不是直接返回注册用户数据,否则造成用户信息泄露
|
|||
throw new UserFriendlyException(L["DuplicateWeChat"]); |
|||
} |
|||
var userName = input.UserName; |
|||
if (userName.IsNullOrWhiteSpace()) |
|||
{ |
|||
userName = "wxid-" + wehchatOpenId.OpenId.ToMd5().ToLower(); |
|||
} |
|||
|
|||
var userEmail = input.EmailAddress;//如果邮件地址不验证,随意写入一个
|
|||
if (userEmail.IsNullOrWhiteSpace()) |
|||
{ |
|||
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(); |
|||
|
|||
var userLogin = new UserLoginInfo(AbpWeChatMiniProgramConsts.ProviderKey, wehchatOpenId.OpenId, AbpWeChatGlobalConsts.DisplayName); |
|||
(await UserManager.AddLoginAsync(user, userLogin)).CheckErrors(); |
|||
|
|||
await CurrentUnitOfWork.SaveChangesAsync(); |
|||
} |
|||
|
|||
public virtual async Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input) |
|||
{ |
|||
await CheckSelfRegistrationAsync(); |
|||
await CheckNewUserPhoneNumberNotBeUsedAsync(input.PhoneNumber); |
|||
|
|||
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["SendRepeatSmsVerifyCode", interval]); |
|||
} |
|||
|
|||
var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsNewUserRegister); |
|||
|
|||
// 安全令牌
|
|||
var securityToken = GuidGenerator.Create().ToString("N"); |
|||
|
|||
var code = TotpService.GenerateCode(Encoding.Unicode.GetBytes(securityToken), securityTokenCacheKey); |
|||
securityTokenCacheItem = new SmsSecurityTokenCacheItem(code.ToString(), securityToken); |
|||
|
|||
await SecurityCodeSender.SendPhoneConfirmedCodeAsync( |
|||
input.PhoneNumber, securityTokenCacheItem.Token, template); |
|||
|
|||
await SecurityTokenCache |
|||
.SetAsync(securityTokenCacheKey, securityTokenCacheItem, |
|||
new DistributedCacheEntryOptions |
|||
{ |
|||
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(interval) |
|||
}); |
|||
} |
|||
|
|||
public virtual async Task RegisterAsync(PhoneRegisterDto input) |
|||
{ |
|||
await CheckSelfRegistrationAsync(); |
|||
await IdentityOptions.SetAsync(); |
|||
await CheckNewUserPhoneNumberNotBeUsedAsync(input.PhoneNumber); |
|||
|
|||
var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.PhoneNumber, "SmsVerifyCode"); |
|||
var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); |
|||
if (securityTokenCacheItem == null) |
|||
{ |
|||
// 验证码过期
|
|||
throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); |
|||
} |
|||
|
|||
// 验证码是否有效
|
|||
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); |
|||
|
|||
(await UserManager.CreateAsync(user, input.Password)).CheckErrors(); |
|||
|
|||
(await UserManager.AddDefaultRolesAsync(user)).CheckErrors(); |
|||
|
|||
await SecurityTokenCache.RemoveAsync(securityTokenCacheKey); |
|||
|
|||
await CurrentUnitOfWork.SaveChangesAsync(); |
|||
|
|||
return; |
|||
} |
|||
} |
|||
// 验证码无效
|
|||
throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); |
|||
} |
|||
|
|||
public virtual async Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input) |
|||
{ |
|||
/* |
|||
* 注解: 微软的重置密码方法通过 UserManager.GeneratePasswordResetTokenAsync 接口生成密码重置Token |
|||
* 而这个Token设计的意义就是用户通过链接来重置密码,所以不适合短信验证 |
|||
* 某些企业是把链接生成一个短链发送短信的,不过这种方式不是很推荐,因为现在是真没几个人敢随便点短信链接的 |
|||
* |
|||
* 此处设计方式为: |
|||
* |
|||
* step1: 例行检查是否重复发送,这一点是很有必要的 |
|||
* step2: 通过已确认的手机号来查询用户,如果用户未确认手机号,那就不能发送,这一点也是很有必要的 |
|||
* step3(重点): 通过 UserManager.GenerateTwoFactorTokenAsync 接口来生成二次认证码,这就相当于伪验证码,只是用于确认用户传递的验证码是否通过 |
|||
* 比起自己生成随机数,这个验证码利用了TOTP算法,有时间限制的 |
|||
* step4(重点): 用户传递验证码后,通过 UserManager.VerifyTwoFactorTokenAsync 接口来校验验证码 |
|||
* 验证通过后,再利用 UserManager.GeneratePasswordResetTokenAsync 接口来生成真正的用于重置密码的Token |
|||
*/ |
|||
|
|||
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["SendRepeatSmsVerifyCode", interval]); |
|||
} |
|||
|
|||
var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsResetPassword); |
|||
// 生成二次认证码
|
|||
var code = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider); |
|||
// 发送短信验证码
|
|||
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) |
|||
}); |
|||
} |
|||
|
|||
public virtual async Task ResetPasswordAsync(PhoneResetPasswordDto input) |
|||
{ |
|||
var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.PhoneNumber, "SmsVerifyCode"); |
|||
var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); |
|||
if (securityTokenCacheItem == null) |
|||
{ |
|||
throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); |
|||
} |
|||
await IdentityOptions.SetAsync(); |
|||
// 传递 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); |
|||
|
|||
await CurrentUnitOfWork.SaveChangesAsync(); |
|||
} |
|||
|
|||
public virtual async Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input) |
|||
{ |
|||
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["SendRepeatSmsVerifyCode", interval]); |
|||
} |
|||
// 传递 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<IdentityUser> GetUserByPhoneNumberAsync(string phoneNumber, bool isConfirmed = true) |
|||
{ |
|||
var user = await UserRepository.FindByPhoneNumberAsync(phoneNumber, isConfirmed, true); |
|||
if (user == null) |
|||
{ |
|||
throw new UserFriendlyException(L["PhoneNumberNotRegisterd"]); |
|||
} |
|||
return user; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 检查是否允许用户注册
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
protected virtual async Task CheckSelfRegistrationAsync() |
|||
{ |
|||
if (!await SettingProvider.IsTrueAsync(Volo.Abp.Account.Settings.AccountSettingNames.IsSelfRegistrationEnabled)) |
|||
{ |
|||
throw new UserFriendlyException(L["SelfRegistrationDisabledMessage"]); |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task CheckNewUserPhoneNumberNotBeUsedAsync(string phoneNumber) |
|||
{ |
|||
if (await UserRepository.IsPhoneNumberUedAsync(phoneNumber)) |
|||
{ |
|||
throw new UserFriendlyException(L["DuplicatePhoneNumber"]); |
|||
} |
|||
} |
|||
|
|||
private void ThowIfInvalidEmailAddress(string inputEmail) |
|||
{ |
|||
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" }) |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
using LINGYUN.Abp.Identity; |
|||
using LINGYUN.Abp.Identity.Security; |
|||
using LINGYUN.Abp.Identity.Settings; |
|||
using LINGYUN.Abp.WeChat; |
|||
using LINGYUN.Abp.WeChat.MiniProgram; |
|||
using LINGYUN.Abp.WeChat.OpenId; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Microsoft.Extensions.Options; |
|||
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.Validation; |
|||
using IIdentityUserRepository = LINGYUN.Abp.Identity.IIdentityUserRepository; |
|||
|
|||
namespace LINGYUN.Abp.Account |
|||
{ |
|||
public class AccountAppService : ApplicationService, IAccountAppService |
|||
{ |
|||
protected ITotpService TotpService { get; } |
|||
protected IdentityUserStore UserStore { get; } |
|||
protected IdentityUserManager UserManager { get; } |
|||
protected IIdentityUserRepository UserRepository { get; } |
|||
protected IUserSecurityCodeSender SecurityCodeSender { get; } |
|||
protected IWeChatOpenIdFinder WeChatOpenIdFinder { get; } |
|||
protected IOptions<IdentityOptions> IdentityOptions { get; } |
|||
protected AbpWeChatMiniProgramOptionsFactory MiniProgramOptionsFactory { get; } |
|||
protected IDistributedCache<SmsSecurityTokenCacheItem> SecurityTokenCache { get; } |
|||
|
|||
public AccountAppService( |
|||
ITotpService totpService, |
|||
IdentityUserStore userStore, |
|||
IdentityUserManager userManager, |
|||
IWeChatOpenIdFinder weChatOpenIdFinder, |
|||
IIdentityUserRepository userRepository, |
|||
IUserSecurityCodeSender securityCodeSender, |
|||
IDistributedCache<SmsSecurityTokenCacheItem> securityTokenCache, |
|||
AbpWeChatMiniProgramOptionsFactory miniProgramOptionsFactory, |
|||
IOptions<IdentityOptions> identityOptions) |
|||
{ |
|||
TotpService = totpService; |
|||
UserStore = userStore; |
|||
UserManager = userManager; |
|||
UserRepository = userRepository; |
|||
WeChatOpenIdFinder = weChatOpenIdFinder; |
|||
SecurityCodeSender = securityCodeSender; |
|||
SecurityTokenCache = securityTokenCache; |
|||
MiniProgramOptionsFactory = miniProgramOptionsFactory; |
|||
IdentityOptions = identityOptions; |
|||
|
|||
LocalizationResource = typeof(AccountResource); |
|||
} |
|||
|
|||
public virtual async Task RegisterAsync(WeChatRegisterDto input) |
|||
{ |
|||
ThowIfInvalidEmailAddress(input.EmailAddress); |
|||
|
|||
await CheckSelfRegistrationAsync(); |
|||
await IdentityOptions.SetAsync(); |
|||
|
|||
var options = await MiniProgramOptionsFactory.CreateAsync(); |
|||
|
|||
var wehchatOpenId = await WeChatOpenIdFinder.FindAsync(input.Code, options.AppId, options.AppSecret); |
|||
|
|||
var user = await UserManager.FindByLoginAsync(AbpWeChatMiniProgramConsts.ProviderName, wehchatOpenId.OpenId); |
|||
if (user != null) |
|||
{ |
|||
// 应该要抛出微信号已注册异常,而不是直接返回注册用户数据,否则造成用户信息泄露
|
|||
throw new UserFriendlyException(L["DuplicateWeChat"]); |
|||
} |
|||
var userName = input.UserName; |
|||
if (userName.IsNullOrWhiteSpace()) |
|||
{ |
|||
userName = "wxid-" + wehchatOpenId.OpenId.ToMd5().ToLower(); |
|||
} |
|||
|
|||
var userEmail = input.EmailAddress;//如果邮件地址不验证,随意写入一个
|
|||
if (userEmail.IsNullOrWhiteSpace()) |
|||
{ |
|||
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(); |
|||
|
|||
var userLogin = new UserLoginInfo(AbpWeChatMiniProgramConsts.ProviderName, wehchatOpenId.OpenId, AbpWeChatGlobalConsts.DisplayName); |
|||
(await UserManager.AddLoginAsync(user, userLogin)).CheckErrors(); |
|||
|
|||
await CurrentUnitOfWork.SaveChangesAsync(); |
|||
} |
|||
|
|||
public virtual async Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input) |
|||
{ |
|||
await CheckSelfRegistrationAsync(); |
|||
await CheckNewUserPhoneNumberNotBeUsedAsync(input.PhoneNumber); |
|||
|
|||
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["SendRepeatSmsVerifyCode", interval]); |
|||
} |
|||
|
|||
var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsNewUserRegister); |
|||
|
|||
// 安全令牌
|
|||
var securityToken = GuidGenerator.Create().ToString("N"); |
|||
|
|||
var code = TotpService.GenerateCode(Encoding.Unicode.GetBytes(securityToken), securityTokenCacheKey); |
|||
securityTokenCacheItem = new SmsSecurityTokenCacheItem(code.ToString(), securityToken); |
|||
|
|||
await SecurityCodeSender.SendPhoneConfirmedCodeAsync( |
|||
input.PhoneNumber, securityTokenCacheItem.Token, template); |
|||
|
|||
await SecurityTokenCache |
|||
.SetAsync(securityTokenCacheKey, securityTokenCacheItem, |
|||
new DistributedCacheEntryOptions |
|||
{ |
|||
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(interval) |
|||
}); |
|||
} |
|||
|
|||
public virtual async Task RegisterAsync(PhoneRegisterDto input) |
|||
{ |
|||
await CheckSelfRegistrationAsync(); |
|||
await IdentityOptions.SetAsync(); |
|||
await CheckNewUserPhoneNumberNotBeUsedAsync(input.PhoneNumber); |
|||
|
|||
var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.PhoneNumber, "SmsVerifyCode"); |
|||
var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); |
|||
if (securityTokenCacheItem == null) |
|||
{ |
|||
// 验证码过期
|
|||
throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); |
|||
} |
|||
|
|||
// 验证码是否有效
|
|||
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); |
|||
|
|||
(await UserManager.CreateAsync(user, input.Password)).CheckErrors(); |
|||
|
|||
(await UserManager.AddDefaultRolesAsync(user)).CheckErrors(); |
|||
|
|||
await SecurityTokenCache.RemoveAsync(securityTokenCacheKey); |
|||
|
|||
await CurrentUnitOfWork.SaveChangesAsync(); |
|||
|
|||
return; |
|||
} |
|||
} |
|||
// 验证码无效
|
|||
throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); |
|||
} |
|||
|
|||
public virtual async Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input) |
|||
{ |
|||
/* |
|||
* 注解: 微软的重置密码方法通过 UserManager.GeneratePasswordResetTokenAsync 接口生成密码重置Token |
|||
* 而这个Token设计的意义就是用户通过链接来重置密码,所以不适合短信验证 |
|||
* 某些企业是把链接生成一个短链发送短信的,不过这种方式不是很推荐,因为现在是真没几个人敢随便点短信链接的 |
|||
* |
|||
* 此处设计方式为: |
|||
* |
|||
* step1: 例行检查是否重复发送,这一点是很有必要的 |
|||
* step2: 通过已确认的手机号来查询用户,如果用户未确认手机号,那就不能发送,这一点也是很有必要的 |
|||
* step3(重点): 通过 UserManager.GenerateTwoFactorTokenAsync 接口来生成二次认证码,这就相当于伪验证码,只是用于确认用户传递的验证码是否通过 |
|||
* 比起自己生成随机数,这个验证码利用了TOTP算法,有时间限制的 |
|||
* step4(重点): 用户传递验证码后,通过 UserManager.VerifyTwoFactorTokenAsync 接口来校验验证码 |
|||
* 验证通过后,再利用 UserManager.GeneratePasswordResetTokenAsync 接口来生成真正的用于重置密码的Token |
|||
*/ |
|||
|
|||
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["SendRepeatSmsVerifyCode", interval]); |
|||
} |
|||
|
|||
var template = await SettingProvider.GetOrNullAsync(IdentitySettingNames.User.SmsResetPassword); |
|||
// 生成二次认证码
|
|||
var code = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider); |
|||
// 发送短信验证码
|
|||
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) |
|||
}); |
|||
} |
|||
|
|||
public virtual async Task ResetPasswordAsync(PhoneResetPasswordDto input) |
|||
{ |
|||
var securityTokenCacheKey = SmsSecurityTokenCacheItem.CalculateCacheKey(input.PhoneNumber, "SmsVerifyCode"); |
|||
var securityTokenCacheItem = await SecurityTokenCache.GetAsync(securityTokenCacheKey); |
|||
if (securityTokenCacheItem == null) |
|||
{ |
|||
throw new UserFriendlyException(L["InvalidSmsVerifyCode"]); |
|||
} |
|||
await IdentityOptions.SetAsync(); |
|||
// 传递 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); |
|||
|
|||
await CurrentUnitOfWork.SaveChangesAsync(); |
|||
} |
|||
|
|||
public virtual async Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input) |
|||
{ |
|||
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["SendRepeatSmsVerifyCode", interval]); |
|||
} |
|||
// 传递 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<IdentityUser> GetUserByPhoneNumberAsync(string phoneNumber, bool isConfirmed = true) |
|||
{ |
|||
var user = await UserRepository.FindByPhoneNumberAsync(phoneNumber, isConfirmed, true); |
|||
if (user == null) |
|||
{ |
|||
throw new UserFriendlyException(L["PhoneNumberNotRegisterd"]); |
|||
} |
|||
return user; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 检查是否允许用户注册
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
protected virtual async Task CheckSelfRegistrationAsync() |
|||
{ |
|||
if (!await SettingProvider.IsTrueAsync(Volo.Abp.Account.Settings.AccountSettingNames.IsSelfRegistrationEnabled)) |
|||
{ |
|||
throw new UserFriendlyException(L["SelfRegistrationDisabledMessage"]); |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task CheckNewUserPhoneNumberNotBeUsedAsync(string phoneNumber) |
|||
{ |
|||
if (await UserRepository.IsPhoneNumberUedAsync(phoneNumber)) |
|||
{ |
|||
throw new UserFriendlyException(L["DuplicatePhoneNumber"]); |
|||
} |
|||
} |
|||
|
|||
private void ThowIfInvalidEmailAddress(string inputEmail) |
|||
{ |
|||
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" }) |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue