diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Account/PhoneCaptchaImageDto.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Account/PhoneCaptchaImageDto.cs index 1428dd6b..94d98d83 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Account/PhoneCaptchaImageDto.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Account/PhoneCaptchaImageDto.cs @@ -2,7 +2,9 @@ { public class PhoneCaptchaImageDto { - public string Phone { get; set; } + public string? Phone { get; set; } + + public string? Email { get; set; } public string Uuid { get; set; } diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Account/RegisterDto.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Account/RegisterDto.cs index f4a337a6..e0fc6f2b 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Account/RegisterDto.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Account/RegisterDto.cs @@ -25,6 +25,8 @@ /// public long? Phone { get; set; } + public string? Email { get; set; } + /// /// 验证码 /// diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/User/ProfileUpdateInputVo.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/User/ProfileUpdateInputVo.cs index f67e9d62..161c82a5 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/User/ProfileUpdateInputVo.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/User/ProfileUpdateInputVo.cs @@ -7,9 +7,9 @@ namespace Yi.Framework.Rbac.Application.Contracts.Dtos.User public string? Name { get; set; } public int? Age { get; set; } public string? Nick { get; set; } - public string? Email { get; set; } + // public string? Email { get; set; } public string? Address { get; set; } - public long? Phone { get; set; } + // public long? Phone { get; set; } public string? Introduction { get; set; } public string? Remark { get; set; } public SexEnum? Sex { get; set; } diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs index 777a4452..f65bb249 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs @@ -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(); private IDistributedCache _phoneCache; + private IDistributedCache _emailCache; private readonly ICaptcha _captcha; private readonly IGuidGenerator _guidGenerator; private readonly RbacOptions _rbacOptions; private readonly IAliyunManger _aliyunManger; - private IDistributedCache _userCache; - private UserManager _userManager; - private IHttpContextAccessor _httpContextAccessor; + private readonly IDistributedCache _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 options, IAliyunManger aliyunManger, - UserManager userManager, IHttpContextAccessor httpContextAccessor) + UserManager userManager, IHttpContextAccessor httpContextAccessor, + IDistributedCache 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 }; } - /// - /// 验证电话号码 - /// - /// - private async Task ValidationPhone(string phone) - { - var res = Regex.IsMatch(phone, @"^\d{11}$"); - if (res == false) - { - throw new UserFriendlyException("手机号码格式错误!请检查"); - } - } - - - /// - /// 手机验证码-注册 - /// - /// - /// - [HttpPost("account/captcha-phone")] - [AllowAnonymous] - public async Task PostCaptchaPhoneForRegisterAsync(PhoneCaptchaImageDto input) - { - return await PostCaptchaPhoneAsync(ValidationPhoneTypeEnum.Register, input); - } - - /// - /// 手机验证码-找回密码 - /// - /// - /// - [HttpPost("account/captcha-phone/repassword")] - public async Task PostCaptchaPhoneForRetrievePasswordAsync(PhoneCaptchaImageDto input) - { - return await PostCaptchaPhoneAsync(ValidationPhoneTypeEnum.RetrievePassword, input); - } - - /// - /// 手机验证码-绑定 - /// - /// - /// - [HttpPost("account/captcha-phone/bind")] - [AllowAnonymous] - public async Task PostCaptchaPhoneForBindAsync(PhoneCaptchaImageDto input) - { - return await PostCaptchaPhoneAsync(ValidationPhoneTypeEnum.Bind, input); - } - - /// - /// 手机验证码-需通过图形验证码 - /// - /// - [RemoteService(isEnabled: false)] - private async Task 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 - }; - } - - /// - /// 校验电话验证码,需要与电话号码绑定 - /// - 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("验证码错误"); - } - /// /// 找回密码 /// @@ -284,6 +184,11 @@ namespace Yi.Framework.Rbac.Application.Services [UnitOfWork] public async Task 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; } - /// /// 注册,需要验证码通过 /// @@ -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); } /// @@ -520,5 +435,241 @@ namespace Yi.Framework.Rbac.Application.Services new AssignmentEventArgs(AssignmentRequirementTypeEnum.UpdateIcon, userId), false); return true; } + + + #region 手机短信相关 + + /// + /// 验证电话号码 + /// + /// + private async Task ValidationPhone(string phone) + { + var res = Regex.IsMatch(phone, @"^\d{11}$"); + if (res == false) + { + throw new UserFriendlyException("手机号码格式错误!请检查"); + } + } + + /// + /// 手机验证码-注册 + /// + /// + /// + [HttpPost("account/captcha-phone/register")] + [AllowAnonymous] + public async Task PostCaptchaPhoneForRegisterAsync(PhoneCaptchaImageDto input) + { + return await InternalPostCaptchaPhoneAsync(ValidationPhoneTypeEnum.Register, input); + } + + /// + /// 手机验证码-找回密码 + /// + /// + /// + [HttpPost("account/captcha-phone/repassword")] + public async Task PostCaptchaPhoneForRetrievePasswordAsync(PhoneCaptchaImageDto input) + { + return await InternalPostCaptchaPhoneAsync(ValidationPhoneTypeEnum.RetrievePassword, input); + } + + /// + /// 手机验证码-绑定 + /// + /// + /// + [HttpPost("account/captcha-phone/bind")] + [AllowAnonymous] + public async Task PostCaptchaPhoneForBindAsync(PhoneCaptchaImageDto input) + { + return await InternalPostCaptchaPhoneAsync(ValidationPhoneTypeEnum.Bind, input); + } + + /// + /// 手机验证码-内部使用-需通过图形验证码 + /// + /// + [RemoteService(isEnabled: false)] + private async Task 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 + }; + } + + /// + /// 校验电话验证码,需要与电话号码绑定 + /// + 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 电子邮箱相关 + + /// + /// 验证电子邮箱 + /// + /// + private async Task ValidationEmail(string email) + { + // 简单邮箱正则表达式 + var res = Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"); + if (res == false) + { + throw new UserFriendlyException("邮箱格式错误!请检查"); + } + } + + /// + /// 邮箱验证码-注册 + /// + /// + /// + [HttpPost("account/captcha-email/register")] + [AllowAnonymous] + public async Task PostCaptchaEmailForRegisterAsync(PhoneCaptchaImageDto input) + { + return await InternalPostCaptchaEmailAsync(ValidationEmailTypeEnum.Register, input); + } + + /// + /// 邮箱验证码-找回密码 + /// + /// + /// + [HttpPost("account/captcha-email/repassword")] + public async Task PostCaptchaEmailForRetrievePasswordAsync(PhoneCaptchaImageDto input) + { + return await InternalPostCaptchaEmailAsync(ValidationEmailTypeEnum.RetrievePassword, input); + } + + /// + /// 邮箱验证码-绑定 + /// + /// + /// + [HttpPost("account/captcha-email/bind")] + [AllowAnonymous] + public async Task PostCaptchaEmailForBindAsync(PhoneCaptchaImageDto input) + { + return await InternalPostCaptchaEmailAsync(ValidationEmailTypeEnum.Bind, input); + } + + /// + /// 邮箱验证码-内部使用-需通过图形验证码 + /// + /// + [RemoteService(isEnabled: false)] + private async Task 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 + }; + } + + /// + /// 校验电子邮箱验证码,需要与电子邮箱绑定 + /// + 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 } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/UserService.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/UserService.cs index 3a386b97..33d97ddf 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/UserService.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/UserService.cs @@ -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), diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Caches/CaptchaEmailCacheItem.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Caches/CaptchaEmailCacheItem.cs new file mode 100644 index 00000000..cde44fc3 --- /dev/null +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Caches/CaptchaEmailCacheItem.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yi.Framework.Rbac.Domain.Shared.Enums; + +namespace Yi.Framework.Rbac.Domain.Shared.Caches +{ + public class CaptchaEmailCacheItem + { + public CaptchaEmailCacheItem(string code) + { + Code = code; + } + + public string Code { get; set; } + } + + public class CaptchaEmailCacheKey + { + public CaptchaEmailCacheKey(ValidationEmailTypeEnum validationPhoneType, string email) + { + Email = email; + ValidationEmailType = validationPhoneType; + } + + public ValidationEmailTypeEnum ValidationEmailType { get; set; } + public string Email { get; set; } + + public override string ToString() + { + return $"Email:{ValidationEmailType.ToString()}:{Email}"; + } + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Enums/ValidationEmailTypeEnum.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Enums/ValidationEmailTypeEnum.cs new file mode 100644 index 00000000..7367c816 --- /dev/null +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Enums/ValidationEmailTypeEnum.cs @@ -0,0 +1,17 @@ +namespace Yi.Framework.Rbac.Domain.Shared.Enums; + +public enum ValidationEmailTypeEnum +{ + /// + /// 注册 + /// + Register, + /// + /// 忘记密码 + /// + RetrievePassword, + /// + /// 绑定 + /// + Bind +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Options/RbacOptions.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Options/RbacOptions.cs index c22becf2..bc8f75ce 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Options/RbacOptions.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Options/RbacOptions.cs @@ -23,6 +23,11 @@ namespace Yi.Framework.Rbac.Domain.Shared.Options /// public bool EnableCaptcha { get; set; } = false; + /// + /// 验证类型 + /// + public CaptchaTypeEnum CaptchaType { get; set; } = CaptchaTypeEnum.Phone; + /// /// 是否开启用户注册功能 /// @@ -33,4 +38,20 @@ namespace Yi.Framework.Rbac.Domain.Shared.Options /// public bool EnableDataBaseBackup { get; set; } = false; } -} + + /// + /// 验证类型 + /// + public enum CaptchaTypeEnum + { + /// + /// 手机号 + /// + Phone = 0, + + /// + /// 邮箱 + /// + Email = 1, + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/UserAggregateRoot.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/UserAggregateRoot.cs index 3f00e048..3f127837 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/UserAggregateRoot.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/UserAggregateRoot.cs @@ -17,14 +17,15 @@ namespace Yi.Framework.Rbac.Domain.Entities { public UserAggregateRoot() { - } - public UserAggregateRoot(string userName, string password, long? phone, string? nick = null) + + public UserAggregateRoot(string userName, string password, long? phone, string? email, string? nick = null) { UserName = userName; EncryPassword.Password = password; Phone = phone; - Nick =string.IsNullOrWhiteSpace(nick)?"萌新-"+userName:nick.Trim(); + Email = email; + Nick = string.IsNullOrWhiteSpace(nick) ? "萌新-" + userName : nick.Trim(); BuildPassword(); } @@ -185,8 +186,10 @@ namespace Yi.Framework.Rbac.Domain.Entities { throw new ArgumentNullException(nameof(EncryPassword.Password)); } + password = EncryPassword.Password; } + EncryPassword.Salt = MD5Helper.GenerateSalt(); EncryPassword.Password = MD5Helper.SHA2Encode(password, EncryPassword.Salt); return this; @@ -203,14 +206,14 @@ namespace Yi.Framework.Rbac.Domain.Entities { throw new ArgumentNullException(EncryPassword.Salt); } + var p = MD5Helper.SHA2Encode(password, EncryPassword.Salt); if (EncryPassword.Password == MD5Helper.SHA2Encode(password, EncryPassword.Salt)) { return true; } + return false; } } - - -} +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs index 28dc9de5..3256ed23 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs @@ -24,7 +24,6 @@ using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.Rbac.Domain.Managers { - /// /// 用户领域服务 /// @@ -62,7 +61,7 @@ namespace Yi.Framework.Rbac.Domain.Managers /// /// /// - public async Task GetTokenByUserIdAsync(Guid userId,Action? getUserInfo=null) + public async Task GetTokenByUserIdAsync(Guid userId, Action? getUserInfo = null) { //获取用户信息 var userInfo = await _userManager.GetInfoAsync(userId); @@ -77,6 +76,7 @@ namespace Yi.Framework.Rbac.Domain.Managers { throw new UserFriendlyException(UserConst.No_Role); } + if (!userInfo.PermissionCodes.Any()) { throw new UserFriendlyException(UserConst.No_Permission); @@ -86,7 +86,7 @@ namespace Yi.Framework.Rbac.Domain.Managers { getUserInfo(userInfo); } - + var accessToken = CreateToken(this.UserInfoToClaim(userInfo)); //将用户信息添加到缓存中,需要考虑的是更改了用户、角色、菜单等整个体系都需要将缓存进行刷新,看具体业务进行选择 return accessToken; @@ -103,12 +103,12 @@ namespace Yi.Framework.Rbac.Domain.Managers var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = kvs.Select(x => new Claim(x.Key, x.Value.ToString())).ToList(); var token = new JwtSecurityToken( - issuer: _jwtOptions.Issuer, - audience: _jwtOptions.Audience, - claims: claims, - expires: DateTime.Now.AddSeconds(_jwtOptions.ExpiresSecondTime), - notBefore: DateTime.Now, - signingCredentials: creds); + issuer: _jwtOptions.Issuer, + audience: _jwtOptions.Audience, + claims: claims, + expires: DateTime.Now.AddSeconds(_jwtOptions.ExpiresSecondTime), + notBefore: DateTime.Now, + signingCredentials: creds); string returnToken = new JwtSecurityTokenHandler().WriteToken(token); return returnToken; @@ -119,22 +119,23 @@ namespace Yi.Framework.Rbac.Domain.Managers var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_refreshJwtOptions.SecurityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); //添加用户id,及刷新token的标识 - var claims = new List { - new Claim(AbpClaimTypes.UserId,userId.ToString()), + var claims = new List + { + new Claim(AbpClaimTypes.UserId, userId.ToString()), new Claim(TokenTypeConst.Refresh, "true") }; var token = new JwtSecurityToken( - issuer: _refreshJwtOptions.Issuer, - audience: _refreshJwtOptions.Audience, - claims: claims, - expires: DateTime.Now.AddSeconds(_refreshJwtOptions.ExpiresSecondTime), - notBefore: DateTime.Now, - signingCredentials: creds); + issuer: _refreshJwtOptions.Issuer, + audience: _refreshJwtOptions.Audience, + claims: claims, + expires: DateTime.Now.AddSeconds(_refreshJwtOptions.ExpiresSecondTime), + notBefore: DateTime.Now, + signingCredentials: creds); string returnToken = new JwtSecurityTokenHandler().WriteToken(token); return returnToken; - } + /// /// 登录校验 /// @@ -142,7 +143,8 @@ namespace Yi.Framework.Rbac.Domain.Managers /// /// /// - public async Task LoginValidationAsync(string userName, string password, Action userAction = null) + public async Task LoginValidationAsync(string userName, string password, + Action userAction = null) { var user = new UserAggregateRoot(); if (await ExistAsync(userName, o => user = o)) @@ -151,12 +153,15 @@ namespace Yi.Framework.Rbac.Domain.Managers { userAction.Invoke(user); } + if (user.EncryPassword.Password == MD5Helper.SHA2Encode(password, user.EncryPassword.Salt)) { return; } + throw new UserFriendlyException(UserConst.Login_Error); } + throw new UserFriendlyException(UserConst.Login_User_No_Exist); } @@ -173,22 +178,22 @@ namespace Yi.Framework.Rbac.Domain.Managers { userAction.Invoke(user); } + //这里为了兼容解决数据库开启了大小写不敏感问题,还要将用户名进行二次校验 if (user != null && user.UserName == userName) { return true; } + return false; } - - + /// /// 令牌转换 /// /// /// - public List> UserInfoToClaim(UserRoleMenuDto dto) { var claims = new List>(); @@ -198,18 +203,24 @@ namespace Yi.Framework.Rbac.Domain.Managers { AddToClaim(claims, TokenTypeConst.DeptId, dto.User.DeptId.ToString()); } + if (dto.User.Email is not null) { AddToClaim(claims, AbpClaimTypes.Email, dto.User.Email); } + if (dto.User.Phone is not null) { AddToClaim(claims, AbpClaimTypes.PhoneNumber, dto.User.Phone.ToString()); } + if (dto.Roles.Count > 0) { - AddToClaim(claims, TokenTypeConst.RoleInfo, JsonConvert.SerializeObject(dto.Roles.Select(x => new RoleTokenInfoModel { Id = x.Id, DataScope = x.DataScope }))); + AddToClaim(claims, TokenTypeConst.RoleInfo, + JsonConvert.SerializeObject(dto.Roles.Select(x => new RoleTokenInfoModel + { Id = x.Id, DataScope = x.DataScope }))); } + if (UserConst.Admin.Equals(dto.User.UserName)) { AddToClaim(claims, TokenTypeConst.Permission, UserConst.AdminPermissionCode); @@ -246,6 +257,7 @@ namespace Yi.Framework.Rbac.Domain.Managers { throw new UserFriendlyException("无效更新!原密码错误!"); } + user.EncryPassword.Password = newPassword; user.BuildPassword(); await _repository.UpdateAsync(user); @@ -271,14 +283,21 @@ namespace Yi.Framework.Rbac.Domain.Managers /// /// /// + /// + /// /// - public async Task RegisterAsync(string userName, string password, long? phone,string? nick) + public async Task RegisterAsync(string userName, string password, long? phone, string? email, + string? nick) { - var user = new UserAggregateRoot(userName, password, phone,nick); - var userId=await _userManager.CreateAsync(user); + if (phone is null && string.IsNullOrWhiteSpace(email)) + { + throw new UserFriendlyException("注册时,电话与邮箱不能同时为空"); + } + + var user = new UserAggregateRoot(userName, password, phone, email, nick); + var userId = await _userManager.CreateAsync(user); await _userManager.SetDefautRoleAsync(user.Id); return userId; } } - -} +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/IAccountManager.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/IAccountManager.cs index d4b8c59d..a8aaf557 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/IAccountManager.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/IAccountManager.cs @@ -14,7 +14,7 @@ namespace Yi.Framework.Rbac.Domain.Managers string CreateRefreshToken(Guid userId); Task GetTokenByUserIdAsync(Guid userId,Action? getUserInfo=null); Task LoginValidationAsync(string userName, string password, Action? userAction = null); - Task RegisterAsync(string userName, string password, long? phone,string? nick); + Task RegisterAsync(string userName, string password, long? phone, string? email, string? nick); Task RestPasswordAsync(Guid userId, string password); Task UpdatePasswordAsync(Guid userId, string newPassword, string oldPassword); } diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Yi.Framework.Rbac.Domain.csproj b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Yi.Framework.Rbac.Domain.csproj index 0680e58f..7fa7d5d9 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Yi.Framework.Rbac.Domain.csproj +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Yi.Framework.Rbac.Domain.csproj @@ -23,6 +23,7 @@ + diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/YiFrameworkRbacDomainModule.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/YiFrameworkRbacDomainModule.cs index 9999ffb9..fd18aade 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/YiFrameworkRbacDomainModule.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/YiFrameworkRbacDomainModule.cs @@ -7,10 +7,10 @@ using Volo.Abp.AspNetCore.SignalR; using Volo.Abp.Caching; using Volo.Abp.DistributedLocking; using Volo.Abp.Domain; +using Volo.Abp.Emailing; using Volo.Abp.Imaging; -using Volo.Abp.Modularity; +using Volo.Abp.MailKit; using Yi.Framework.Caching.FreeRedis; -using Yi.Framework.Mapster; using Yi.Framework.Rbac.Domain.Authorization; using Yi.Framework.Rbac.Domain.Operlog; using Yi.Framework.Rbac.Domain.Shared; @@ -26,7 +26,8 @@ namespace Yi.Framework.Rbac.Domain typeof(AbpDddDomainModule), typeof(AbpCachingModule), typeof(AbpImagingImageSharpModule), - typeof(AbpDistributedLockingModule) + typeof(AbpDistributedLockingModule), + typeof(AbpMailKitModule) )] public class YiFrameworkRbacDomainModule : AbpModule { diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json b/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json index 41ad6324..e62f5eab 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json @@ -25,7 +25,17 @@ }, //配置 "Settings": { - "Test": "hello" + "Test": "hello", + //邮箱 + "Abp.Mailing.Smtp.Host": "127.0.0.1", + "Abp.Mailing.Smtp.Port": "25", + "Abp.Mailing.Smtp.UserName": "", + "Abp.Mailing.Smtp.Password": "", + "Abp.Mailing.Smtp.Domain": "", + "Abp.Mailing.Smtp.EnableSsl": "false", + "Abp.Mailing.Smtp.UseDefaultCredentials": "true", + "Abp.Mailing.DefaultFromAddress": "noreply@abp.io", + "Abp.Mailing.DefaultFromDisplayName": "ABP application" }, //数据库类型列表 "DbList": [ @@ -95,6 +105,8 @@ "AdminPassword": "123456", //是否开启验证码验证 "EnableCaptcha": true, + //验证类型:email/Phone + "CaptchaType": "Email", //是否开启注册功能 "EnableRegister": false, //开启定时数据库备份 @@ -107,15 +119,5 @@ ], "Endpoint": "https://xxx.com/v1", "ApiKey": "sk-xxxxxx" - }, - //AI网关 - "AiGateWay": { - "Chats": { - "AzureChatService": { - "ModelIds": ["gpt-4o"], - "Endpoint": "https://xxx.com/v1", - "ApiKey": "sk-xxxxxx" - } - } } } diff --git a/Yi.Bbs.Vue3/src/apis/accountApi.js b/Yi.Bbs.Vue3/src/apis/accountApi.js index b46cf302..9514b137 100644 --- a/Yi.Bbs.Vue3/src/apis/accountApi.js +++ b/Yi.Bbs.Vue3/src/apis/accountApi.js @@ -85,7 +85,19 @@ export function getCodeImg() { // 获取短信验证码 export function getCodePhone(phoneForm) { return request({ - url: "/account/captcha-phone", + url: "/account/captcha-phone/register", + headers: { + isToken: false, + }, + method: "post", + timeout: 20000, + data: phoneForm, + }); +} +// 获取邮箱验证码 +export function getCodeEmail(phoneForm) { + return request({ + url: "/account/captcha-email/register", headers: { isToken: false, }, diff --git a/Yi.Bbs.Vue3/src/apis/auth.js b/Yi.Bbs.Vue3/src/apis/auth.js index ad037ca1..ea6022a9 100644 --- a/Yi.Bbs.Vue3/src/apis/auth.js +++ b/Yi.Bbs.Vue3/src/apis/auth.js @@ -59,7 +59,7 @@ export function userLogout() { */ export function getCodePhone(data) { return request({ - url: `/account/captcha-phone`, + url: `/account/captcha-phone/register`, method: "post", data, }); diff --git a/Yi.Bbs.Vue3/src/views/login/register.vue b/Yi.Bbs.Vue3/src/views/login/register.vue index 2e1b2cd6..9c3c0d06 100644 --- a/Yi.Bbs.Vue3/src/views/login/register.vue +++ b/Yi.Bbs.Vue3/src/views/login/register.vue @@ -2,7 +2,7 @@ // 注册逻辑 import {computed, reactive, ref} from "vue"; -import {getCodePhone} from "@/apis/accountApi"; +import {getCodeEmail} from "@/apis/accountApi"; import useAuths from "@/hooks/useAuths"; import {useRoute, useRouter} from "vue-router"; import useUserStore from "@/stores/user"; @@ -20,15 +20,16 @@ const codeUUid = computed(() => useUserStore().codeUUid); const passwordConfirm = ref(""); const registerForm = reactive({ userName: "", - phone: "", + //phone: "", password: "", uuid: "", code: "", - nick:"" + nick:"", + email: "", }); const phoneForm=reactive({ code:"", - phone:"", + email:"", uuid:codeUUid }); const registerRules = reactive({ @@ -39,7 +40,7 @@ const registerRules = reactive({ { required: true, message: "请输入用户名", trigger: "blur" }, { min: 2, message: "用户名需大于两位", trigger: "blur" }, ], - phone: [{ required: true, message: "请输入手机号", trigger: "blur" }], + email: [{ required: true, message: "请输入邮箱", trigger: "blur" }], code: [{ required: true, message: "请输入验证码", trigger: "blur" }], password: [ { required: true, message: "请输入新的密码", trigger: "blur" }, @@ -52,7 +53,7 @@ const register = async (formEl) => { if (valid) { try { - if (registerForm.password != passwordConfirm.value) { + if (registerForm.password !== passwordConfirm.value) { ElMessage.error("两次密码输入不一致"); return; } @@ -72,7 +73,7 @@ const register = async (formEl) => { //验证码 -const codeInfo = ref("发送短信"); +const codeInfo = ref("发送邮箱"); const isDisabledCode = ref(false); //点击验证码 @@ -80,9 +81,9 @@ const handleGetCodeImage=()=>{ useUserStore().updateCodeImage(); } -//点击手机发送短信 +//点击手机发送邮箱 const clickPhoneCaptcha=()=>{ - if (registerForm.phone !== "") + if (registerForm.email !== "") { isDisabledCode.value=true; handleGetCodeImage(); @@ -90,7 +91,7 @@ const clickPhoneCaptcha=()=>{ } else { ElMessage({ - message: `请先输入手机号`, + message: `请先输入邮箱`, type: "warning", }); } @@ -101,14 +102,14 @@ const handleSignInNow=()=>{ router.push("/login"); } const captcha = async () => { - if (registerForm.phone!==""&&phoneForm.code!=="") + if (registerForm.email!==""&&phoneForm.code!=="") { - phoneForm.phone=registerForm.phone; - const { data } = await getCodePhone(phoneForm); + phoneForm.email=registerForm.email; + const { data } = await getCodeEmail(phoneForm); registerForm.uuid = data.uuid; codeDialogVisible.value=false; ElMessage({ - message: `已向${registerForm.phone}发送验证码,请注意查收`, + message: `已向${registerForm.email}发送验证码,请注意查收`, type: "success", }); isDisabledCode.value = true; @@ -171,16 +172,16 @@ const captcha = async () => {
-

*电话

- +

*邮箱

+
- +
-

*短信验证码

+

*邮箱验证码

@@ -211,7 +212,7 @@ const captcha = async () => { @@ -223,7 +224,8 @@ const captcha = async () => { -

由于国内短信严格程度在2025年5月连续加强3次,你的验证码有一定概率被运营商拦截

+ +

请检查你的邮箱垃圾箱,可能在那

如果未收到验证码,请联系微信chengzilaoge520 站长进行手动创建

diff --git a/Yi.Bbs.Vue3/src/views/profile/UserInfo.vue b/Yi.Bbs.Vue3/src/views/profile/UserInfo.vue index d2d9f7c9..6af58a76 100644 --- a/Yi.Bbs.Vue3/src/views/profile/UserInfo.vue +++ b/Yi.Bbs.Vue3/src/views/profile/UserInfo.vue @@ -7,10 +7,10 @@ - + - + @@ -46,22 +46,22 @@ const userRef = ref(null); const rules = ref({ nick: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }], - email: [ - { required: true, message: "邮箱地址不能为空", trigger: "blur" }, - { - type: "email", - message: "请输入正确的邮箱地址", - trigger: ["blur", "change"], - }, - ], - phone: [ - { required: true, message: "手机号码不能为空", trigger: "blur" }, - { - pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, - message: "请输入正确的手机号码", - trigger: "blur", - }, - ], + // email: [ + // { required: true, message: "邮箱地址不能为空", trigger: "blur" }, + // { + // type: "email", + // message: "请输入正确的邮箱地址", + // trigger: ["blur", "change"], + // }, + // ], + // phone: [ + // { required: true, message: "手机号码不能为空", trigger: "blur" }, + // { + // pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, + // message: "请输入正确的手机号码", + // trigger: "blur", + // }, + // ], }); /** 提交按钮 */