feat: 支持邮箱注册功能
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
using System.Net.Mail;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Lazy.Captcha.Core;
|
||||
using Mapster;
|
||||
@@ -9,6 +12,7 @@ using Microsoft.Extensions.Options;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.Authorization;
|
||||
using Volo.Abp.Caching;
|
||||
using Volo.Abp.Emailing;
|
||||
using Volo.Abp.EventBus.Local;
|
||||
using Volo.Abp.Guids;
|
||||
using Volo.Abp.Uow;
|
||||
@@ -34,13 +38,15 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
{
|
||||
protected ILocalEventBus LocalEventBus => LazyServiceProvider.LazyGetRequiredService<ILocalEventBus>();
|
||||
private IDistributedCache<CaptchaPhoneCacheItem, CaptchaPhoneCacheKey> _phoneCache;
|
||||
private IDistributedCache<CaptchaEmailCacheItem, CaptchaEmailCacheKey> _emailCache;
|
||||
private readonly ICaptcha _captcha;
|
||||
private readonly IGuidGenerator _guidGenerator;
|
||||
private readonly RbacOptions _rbacOptions;
|
||||
private readonly IAliyunManger _aliyunManger;
|
||||
private IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> _userCache;
|
||||
private UserManager _userManager;
|
||||
private IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> _userCache;
|
||||
private readonly UserManager _userManager;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IEmailSender _emailSender;
|
||||
|
||||
public AccountService(IUserRepository userRepository,
|
||||
ICurrentUser currentUser,
|
||||
@@ -52,7 +58,8 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
IGuidGenerator guidGenerator,
|
||||
IOptions<RbacOptions> options,
|
||||
IAliyunManger aliyunManger,
|
||||
UserManager userManager, IHttpContextAccessor httpContextAccessor)
|
||||
UserManager userManager, IHttpContextAccessor httpContextAccessor,
|
||||
IDistributedCache<CaptchaEmailCacheItem, CaptchaEmailCacheKey> emailCache, IEmailSender emailSender)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_currentUser = currentUser;
|
||||
@@ -66,6 +73,8 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
_userCache = userCache;
|
||||
_userManager = userManager;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_emailCache = emailCache;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,115 +176,6 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
return new CaptchaImageDto { Img = captcha.Bytes, Uuid = uuid, IsEnableCaptcha = enableCaptcha };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证电话号码
|
||||
/// </summary>
|
||||
/// <param name="phone"></param>
|
||||
private async Task ValidationPhone(string phone)
|
||||
{
|
||||
var res = Regex.IsMatch(phone, @"^\d{11}$");
|
||||
if (res == false)
|
||||
{
|
||||
throw new UserFriendlyException("手机号码格式错误!请检查");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 手机验证码-注册
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("account/captcha-phone")]
|
||||
[AllowAnonymous]
|
||||
public async Task<object> PostCaptchaPhoneForRegisterAsync(PhoneCaptchaImageDto input)
|
||||
{
|
||||
return await PostCaptchaPhoneAsync(ValidationPhoneTypeEnum.Register, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手机验证码-找回密码
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("account/captcha-phone/repassword")]
|
||||
public async Task<object> PostCaptchaPhoneForRetrievePasswordAsync(PhoneCaptchaImageDto input)
|
||||
{
|
||||
return await PostCaptchaPhoneAsync(ValidationPhoneTypeEnum.RetrievePassword, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手机验证码-绑定
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("account/captcha-phone/bind")]
|
||||
[AllowAnonymous]
|
||||
public async Task<object> PostCaptchaPhoneForBindAsync(PhoneCaptchaImageDto input)
|
||||
{
|
||||
return await PostCaptchaPhoneAsync(ValidationPhoneTypeEnum.Bind, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手机验证码-需通过图形验证码
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[RemoteService(isEnabled: false)]
|
||||
private async Task<object> PostCaptchaPhoneAsync(ValidationPhoneTypeEnum validationPhoneType,
|
||||
PhoneCaptchaImageDto input)
|
||||
{
|
||||
//验证uuid 和 验证码
|
||||
ValidationImageCaptcha(input.Uuid, input.Code);
|
||||
|
||||
await ValidationPhone(input.Phone);
|
||||
|
||||
if (validationPhoneType == ValidationPhoneTypeEnum.Register &&
|
||||
await _userRepository.IsAnyAsync(x => x.Phone.ToString() == input.Phone))
|
||||
{
|
||||
throw new UserFriendlyException("该手机号已被注册!");
|
||||
}
|
||||
|
||||
var value = await _phoneCache.GetAsync(new CaptchaPhoneCacheKey(validationPhoneType, input.Phone));
|
||||
|
||||
//防止暴刷
|
||||
if (value is not null)
|
||||
{
|
||||
throw new UserFriendlyException($"{input.Phone}已发送过验证码,10分钟后可重试");
|
||||
}
|
||||
|
||||
//生成一个4位数的验证码
|
||||
//发送短信,同时生成uuid
|
||||
////key: 电话号码 value:验证码+uuid
|
||||
var code = Guid.NewGuid().ToString().Substring(0, 4);
|
||||
var uuid = Guid.NewGuid();
|
||||
await _aliyunManger.SendSmsAsync(input.Phone, code);
|
||||
|
||||
await _phoneCache.SetAsync(new CaptchaPhoneCacheKey(validationPhoneType, input.Phone),
|
||||
new CaptchaPhoneCacheItem(code),
|
||||
new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(10) });
|
||||
return new
|
||||
{
|
||||
Uuid = uuid
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验电话验证码,需要与电话号码绑定
|
||||
/// </summary>
|
||||
public async Task ValidationPhoneCaptchaAsync(ValidationPhoneTypeEnum validationPhoneType, long phone,
|
||||
string code)
|
||||
{
|
||||
var item = await _phoneCache.GetAsync(new CaptchaPhoneCacheKey(validationPhoneType, phone.ToString()));
|
||||
if (item is not null && item.Code.Equals($"{code}"))
|
||||
{
|
||||
//成功,需要清空
|
||||
await _phoneCache.RemoveAsync(new CaptchaPhoneCacheKey(validationPhoneType, code.ToString()));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UserFriendlyException("验证码错误");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 找回密码
|
||||
/// </summary>
|
||||
@@ -284,6 +184,11 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
[UnitOfWork]
|
||||
public async Task<string> PostRetrievePasswordAsync(RetrievePasswordDto input)
|
||||
{
|
||||
if (_rbacOptions.CaptchaType == CaptchaTypeEnum.Email)
|
||||
{
|
||||
throw new UserFriendlyException("当前模式,不允许手机号找回密码,请联系管理员");
|
||||
}
|
||||
|
||||
//校验验证码,根据电话号码获取 value,比对验证码已经uuid
|
||||
await ValidationPhoneCaptchaAsync(ValidationPhoneTypeEnum.RetrievePassword, input.Phone, input.Code);
|
||||
|
||||
@@ -298,7 +203,6 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
return entity.UserName;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 注册,需要验证码通过
|
||||
/// </summary>
|
||||
@@ -313,9 +217,9 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
throw new UserFriendlyException("该系统暂未开放注册功能");
|
||||
}
|
||||
|
||||
if (input.Phone is null)
|
||||
if (input.Phone is null && input.Email is null)
|
||||
{
|
||||
throw new UserFriendlyException("手机号不能为空");
|
||||
throw new UserFriendlyException("手机号和邮箱不能为空");
|
||||
}
|
||||
|
||||
//临时账号
|
||||
@@ -326,12 +230,23 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
|
||||
if (_rbacOptions.EnableCaptcha)
|
||||
{
|
||||
//校验验证码,根据电话号码获取 value,比对验证码已经uuid
|
||||
await ValidationPhoneCaptchaAsync(ValidationPhoneTypeEnum.Register, input.Phone.Value, input.Code);
|
||||
switch (_rbacOptions.CaptchaType)
|
||||
{
|
||||
case CaptchaTypeEnum.Phone:
|
||||
//校验验证码,根据电话号码获取 value,比对验证码已经uuid
|
||||
await ValidationPhoneCaptchaAsync(ValidationPhoneTypeEnum.Register, input.Phone.Value,
|
||||
input.Code);
|
||||
break;
|
||||
case CaptchaTypeEnum.Email:
|
||||
//校验验证码,根据电子邮箱获取 value,比对验证码已经uuid
|
||||
await ValidationEmailCaptchaAsync(ValidationEmailTypeEnum.Register, input.Email, input.Code);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//注册之后,免再次登录,直接给前端token
|
||||
var userId = await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Nick);
|
||||
var userId = await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Email,
|
||||
input.Nick);
|
||||
return await this.PostLoginAsync(userId);
|
||||
}
|
||||
|
||||
@@ -344,7 +259,7 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
public async Task PostTempRegisterAsync(RegisterDto input)
|
||||
{
|
||||
//注册领域逻辑
|
||||
await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Nick);
|
||||
await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Email, input.Nick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -520,5 +435,241 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
new AssignmentEventArgs(AssignmentRequirementTypeEnum.UpdateIcon, userId), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
#region 手机短信相关
|
||||
|
||||
/// <summary>
|
||||
/// 验证电话号码
|
||||
/// </summary>
|
||||
/// <param name="phone"></param>
|
||||
private async Task ValidationPhone(string phone)
|
||||
{
|
||||
var res = Regex.IsMatch(phone, @"^\d{11}$");
|
||||
if (res == false)
|
||||
{
|
||||
throw new UserFriendlyException("手机号码格式错误!请检查");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手机验证码-注册
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("account/captcha-phone/register")]
|
||||
[AllowAnonymous]
|
||||
public async Task<object> PostCaptchaPhoneForRegisterAsync(PhoneCaptchaImageDto input)
|
||||
{
|
||||
return await InternalPostCaptchaPhoneAsync(ValidationPhoneTypeEnum.Register, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手机验证码-找回密码
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("account/captcha-phone/repassword")]
|
||||
public async Task<object> PostCaptchaPhoneForRetrievePasswordAsync(PhoneCaptchaImageDto input)
|
||||
{
|
||||
return await InternalPostCaptchaPhoneAsync(ValidationPhoneTypeEnum.RetrievePassword, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手机验证码-绑定
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("account/captcha-phone/bind")]
|
||||
[AllowAnonymous]
|
||||
public async Task<object> PostCaptchaPhoneForBindAsync(PhoneCaptchaImageDto input)
|
||||
{
|
||||
return await InternalPostCaptchaPhoneAsync(ValidationPhoneTypeEnum.Bind, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手机验证码-内部使用-需通过图形验证码
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[RemoteService(isEnabled: false)]
|
||||
private async Task<object> InternalPostCaptchaPhoneAsync(ValidationPhoneTypeEnum validationPhoneType,
|
||||
PhoneCaptchaImageDto input)
|
||||
{
|
||||
//验证uuid 和 验证码
|
||||
ValidationImageCaptcha(input.Uuid, input.Code);
|
||||
|
||||
await ValidationPhone(input.Phone);
|
||||
|
||||
if (validationPhoneType == ValidationPhoneTypeEnum.Register &&
|
||||
await _userRepository.IsAnyAsync(x => x.Phone.ToString() == input.Phone))
|
||||
{
|
||||
throw new UserFriendlyException("该手机号已被注册!");
|
||||
}
|
||||
|
||||
var value = await _phoneCache.GetAsync(new CaptchaPhoneCacheKey(validationPhoneType, input.Phone));
|
||||
|
||||
//防止暴刷
|
||||
if (value is not null)
|
||||
{
|
||||
throw new UserFriendlyException($"{input.Phone}已发送过验证码,10分钟后可重试");
|
||||
}
|
||||
|
||||
//生成一个4位数的验证码
|
||||
//发送短信,同时生成uuid
|
||||
////key: 电话号码 value:验证码+uuid
|
||||
var code = Guid.NewGuid().ToString().Substring(0, 4);
|
||||
var uuid = Guid.NewGuid();
|
||||
await _aliyunManger.SendSmsAsync(input.Phone, code);
|
||||
|
||||
await _phoneCache.SetAsync(new CaptchaPhoneCacheKey(validationPhoneType, input.Phone),
|
||||
new CaptchaPhoneCacheItem(code),
|
||||
new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(10) });
|
||||
return new
|
||||
{
|
||||
Uuid = uuid
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验电话验证码,需要与电话号码绑定
|
||||
/// </summary>
|
||||
public async Task ValidationPhoneCaptchaAsync(ValidationPhoneTypeEnum validationPhoneType, long phone,
|
||||
string code)
|
||||
{
|
||||
var item = await _phoneCache.GetAsync(new CaptchaPhoneCacheKey(validationPhoneType, phone.ToString()));
|
||||
if (item is not null && item.Code.Equals($"{code}"))
|
||||
{
|
||||
//成功,需要清空
|
||||
await _phoneCache.RemoveAsync(new CaptchaPhoneCacheKey(validationPhoneType, phone.ToString()));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UserFriendlyException("验证码错误");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 电子邮箱相关
|
||||
|
||||
/// <summary>
|
||||
/// 验证电子邮箱
|
||||
/// </summary>
|
||||
/// <param name="email"></param>
|
||||
private async Task ValidationEmail(string email)
|
||||
{
|
||||
// 简单邮箱正则表达式
|
||||
var res = Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
|
||||
if (res == false)
|
||||
{
|
||||
throw new UserFriendlyException("邮箱格式错误!请检查");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱验证码-注册
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("account/captcha-email/register")]
|
||||
[AllowAnonymous]
|
||||
public async Task<object> PostCaptchaEmailForRegisterAsync(PhoneCaptchaImageDto input)
|
||||
{
|
||||
return await InternalPostCaptchaEmailAsync(ValidationEmailTypeEnum.Register, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱验证码-找回密码
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("account/captcha-email/repassword")]
|
||||
public async Task<object> PostCaptchaEmailForRetrievePasswordAsync(PhoneCaptchaImageDto input)
|
||||
{
|
||||
return await InternalPostCaptchaEmailAsync(ValidationEmailTypeEnum.RetrievePassword, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱验证码-绑定
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("account/captcha-email/bind")]
|
||||
[AllowAnonymous]
|
||||
public async Task<object> PostCaptchaEmailForBindAsync(PhoneCaptchaImageDto input)
|
||||
{
|
||||
return await InternalPostCaptchaEmailAsync(ValidationEmailTypeEnum.Bind, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱验证码-内部使用-需通过图形验证码
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[RemoteService(isEnabled: false)]
|
||||
private async Task<object> InternalPostCaptchaEmailAsync(ValidationEmailTypeEnum validationEmailType,
|
||||
PhoneCaptchaImageDto input)
|
||||
{
|
||||
//验证uuid 和 验证码
|
||||
ValidationImageCaptcha(input.Uuid, input.Code);
|
||||
|
||||
await ValidationEmail(input.Email);
|
||||
|
||||
if (validationEmailType == ValidationEmailTypeEnum.Register)
|
||||
{
|
||||
//处理大小写问题
|
||||
var emailOrNull = await _userRepository._DbQueryable.Where(x => x.Email == input.Email)
|
||||
.Select(x => x.Email)
|
||||
.FirstAsync();
|
||||
if (emailOrNull is not null && emailOrNull.Equals(input.Email))
|
||||
{
|
||||
throw new UserFriendlyException("该邮箱已被注册!");
|
||||
}
|
||||
}
|
||||
|
||||
var value = await _emailCache.GetAsync(new CaptchaEmailCacheKey(validationEmailType, input.Email));
|
||||
|
||||
//防止暴刷
|
||||
if (value is not null)
|
||||
{
|
||||
throw new UserFriendlyException($"{input.Email}已发送过验证码,10分钟后可重试");
|
||||
}
|
||||
|
||||
//生成一个4位数的验证码
|
||||
//发送邮箱,同时生成uuid
|
||||
////key: 邮箱 value:验证码+uuid
|
||||
var code = Guid.NewGuid().ToString().Substring(0, 4);
|
||||
var uuid = Guid.NewGuid();
|
||||
//await _aliyunManger.SendSmsAsync(input.Phone, code);
|
||||
//发送邮件
|
||||
await _emailSender.SendAsync(input.Email,
|
||||
"意社区官方邮件",
|
||||
$"欢迎加入我们,您的验证码为 {code} ,该验证码10分钟内有效,请勿泄露于他人。");
|
||||
|
||||
await _emailCache.SetAsync(new CaptchaEmailCacheKey(validationEmailType, input.Email),
|
||||
new CaptchaEmailCacheItem(code),
|
||||
new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(10) });
|
||||
return new
|
||||
{
|
||||
Uuid = uuid
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验电子邮箱验证码,需要与电子邮箱绑定
|
||||
/// </summary>
|
||||
public async Task ValidationEmailCaptchaAsync(ValidationEmailTypeEnum validationEmailType, string email,
|
||||
string code)
|
||||
{
|
||||
var item = await _emailCache.GetAsync(new CaptchaEmailCacheKey(validationEmailType, email.ToString()));
|
||||
if (item is not null && item.Code.Equals($"{code}"))
|
||||
{
|
||||
//成功,需要清空
|
||||
await _emailCache.RemoveAsync(new CaptchaEmailCacheKey(validationEmailType, email.ToString()));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UserFriendlyException("验证码错误");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -191,7 +191,7 @@ namespace Yi.Framework.Rbac.Application.Services.System
|
||||
await _repository.UpdateAsync(entity);
|
||||
var dto = await MapToGetOutputDtoAsync(entity);
|
||||
//发布更新昵称任务事件
|
||||
if (input.Nick != entity.Icon)
|
||||
if (input.Nick != entity.Nick)
|
||||
{
|
||||
await this.LocalEventBus.PublishAsync(
|
||||
new AssignmentEventArgs(AssignmentRequirementTypeEnum.UpdateNick, _currentUser.GetId(), input.Nick),
|
||||
|
||||
Reference in New Issue
Block a user