新增用户重置密码和修改密码
This commit is contained in:
lq1405 2025-03-22 20:48:40 +08:00
parent 57402e0dda
commit a23984c9f5
9 changed files with 380 additions and 20 deletions

View File

@ -0,0 +1,78 @@
using System.Security.Cryptography;
using System.Text;
namespace LMS.Common.Password
{
public static class PasswordGenerator
{
private const string LowercaseChars = "abcdefghijklmnopqrstuvwxyz";
private const string UppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private const string NumberChars = "0123456789";
private const string SpecialChars = "@$!%*?.&";
private const string AllChars = LowercaseChars + UppercaseChars + NumberChars + SpecialChars;
/// <summary>
/// 生成指定长度的随机密码
/// </summary>
/// <param name="length">密码长度</param>
/// <returns>随机生成的密码</returns>
public static string GeneratePassword(int length)
{
if (length < 4)
{
throw new ArgumentException("密码长度必须至少为4个字符", nameof(length));
}
// 使用加密安全的随机数生成器
using var rng = RandomNumberGenerator.Create();
// 确保密码包含至少一个小写字母、一个大写字母、一个数字和一个特殊字符
var password = new StringBuilder();
// 添加每种类型的至少一个字符
password.Append(GetRandomChar(LowercaseChars, rng));
password.Append(GetRandomChar(UppercaseChars, rng));
password.Append(GetRandomChar(NumberChars, rng));
password.Append(GetRandomChar(SpecialChars, rng));
// 添加剩余的随机字符
for (int i = 4; i < length; i++)
{
password.Append(GetRandomChar(AllChars, rng));
}
// 打乱字符顺序
return ShuffleString(password.ToString(), rng);
}
/// <summary>
/// 从指定字符集中获取一个随机字符
/// </summary>
private static char GetRandomChar(string chars, RandomNumberGenerator rng)
{
byte[] data = new byte[1];
rng.GetBytes(data);
return chars[data[0] % chars.Length];
}
/// <summary>
/// 打乱字符串中字符的顺序
/// </summary>
private static string ShuffleString(string input, RandomNumberGenerator rng)
{
char[] array = input.ToCharArray();
int n = array.Length;
while (n > 1)
{
byte[] box = new byte[1];
rng.GetBytes(box);
int k = box[0] % n;
n--;
char temp = array[n];
array[n] = array[k];
array[k] = temp;
}
return new string(array);
}
}
}

View File

@ -9,14 +9,37 @@
<!DOCTYPE html>
<html>
<body>
<h1>LMS Registration Code</h1>
<p>Hi there,</p>
<p>Your code is <strong>{RegisterCode}</strong></p>
<p>The code is valid for 10 minutes, if it is not you, please ignore it.</p>
<h1>LMS注册验证码</h1>
<p></p>
<p> <strong>{RegisterCode}</strong></p>
<p> 10 </p>
</body>
</html>
""";
public const string ResetPasswordHtmlTemplates = """
<!DOCTYPE html>
<html>
<body>
<h1>LMS重置密码验证码</h1>
<p></p>
<p> <strong>{ResetPasswordCode}</strong></p>
<p> 10 </p>
</body>
</html>
""";
public const string ResetpasswordSuccessMail = """
<!DOCTYPE html>
<html>
<body>
<h1>LMS重置密码成功</h1>
<p></p>
<p></p>
<p> <strong>{NewPassword}</strong></p>
</body>
</html>
""";
/// <summary>
/// 替换模板的占位符

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Net.Mail;
using static LMS.Common.Enums.ResponseCodeEnum;
namespace LMS.service.Controllers
@ -142,6 +143,49 @@ namespace LMS.service.Controllers
#endregion
#region
[HttpPost]
public async Task<ActionResult<APIResponseModel<string>>> SendResetPasswordCode([FromBody] EmailVerificationService.SendVerificationCodeDto model)
{
if (!ModelState.IsValid)
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError);
return await _loginService.SendResetPasswordCode(model);
}
#endregion
#region
[HttpPost("{mail}/{code}")]
public async Task<ActionResult<APIResponseModel<string>>> ResetPassword(string mail, string code)
{
// 校验邮箱是不是有效
if (string.IsNullOrWhiteSpace(mail))
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的邮箱");
try
{
// 尝试创建MailAddress实例如果成功则表示邮箱格式正确
MailAddress mailAddress = new(mail);
bool isMail = mailAddress.Address == mail;
if (!isMail)
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的邮箱");
return await _loginService.UserResetPassword(mail, code);
}
catch (FormatException)
{
// 创建失败表示邮箱格式不正确
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的邮箱");
}
}
#endregion
#region token
[HttpPost]
@ -256,7 +300,7 @@ namespace LMS.service.Controllers
#endregion
#region
#region
[HttpPost("{id}")]
[Authorize]

View File

@ -23,7 +23,13 @@ public class EmailVerificationService
public string Email { get; set; }
}
// 生成并发送验证码
#region
/// <summary>
/// 发送注册验证码
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
public async Task SendVerificationCodeAsync(string email)
{
// 生成6位数验证码
@ -46,7 +52,12 @@ public class EmailVerificationService
await _emailService.SendEmailAsync(email, "LMS注册验证码", emailBody);
}
// 验证用户提交的验证码
/// <summary>
/// 验证用户提交的验证码
/// </summary>
/// <param name="email"></param>
/// <param name="code"></param>
/// <returns></returns>
public async Task<bool> VerifyCodeAsync(string email, string code)
{
var storedCode = await _cache.GetStringAsync($"EmailVerification_{email}");
@ -64,8 +75,84 @@ public class EmailVerificationService
return false;
}
/// <summary>
/// 生成一个6位数的验证码
/// </summary>
/// <returns></returns>
private string GenerateVerificationCode()
{
return _random.Next(100000, 999999).ToString();
}
#endregion
#region
/// <summary>
/// 发送重置密码验证码
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
public async Task SendResetPasswordCodeAsync(string email)
{
// 生成6位数验证码
string resetPasswordCode = GenerateVerificationCode();
// 将验证码保存到分布式缓存设置10分钟过期
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
};
await _cache.SetStringAsync($"ResetPassword_{email}", resetPasswordCode, options);
var emailBody = EmailTemplateService.ReplaceTemplate(EmailTemplateService.ResetPasswordHtmlTemplates, new Dictionary<string, string>
{
{ "ResetPasswordCode", resetPasswordCode }
});
// 发送验证码邮件
await _emailService.SendEmailAsync(email, "LMS重置密码验证码", emailBody);
}
/// <summary>
/// 严重用户提交成的重置密码验证码
/// </summary>
/// <param name="email"></param>
/// <param name="code"></param>
/// <returns></returns>
public async Task<bool> VerifyResetPasswordAsync(string email, string code)
{
var storedCode = await _cache.GetStringAsync($"ResetPassword_{email}");
if (string.IsNullOrEmpty(storedCode))
return false;
// 使用完就删除验证码
if (storedCode == code)
{
await _cache.RemoveAsync($"ResetPassword_{email}");
return true;
}
return false;
}
#endregion
#region
/// <summary>
/// 发送重置密码验证码
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
public async Task SendResetpasswordSuccessMail(string email, string newPassword)
{
var emailBody = EmailTemplateService.ReplaceTemplate(EmailTemplateService.ResetpasswordSuccessMail, new Dictionary<string, string>
{
{ "NewPassword", newPassword }
});
// 发送验证码邮件
await _emailService.SendEmailAsync(email, "LMS重置密码成功", emailBody);
}
#endregion
}

View File

@ -391,8 +391,11 @@ namespace LMS.service.Service
query = query.Where(x => filteredOwnerIds.Contains(x.UserID));
}
if (isSuperAdmin)
{
if (isAdmin && !isSuperAdmin)
}
else if (isAdmin)
{
// 除了超级管理员的代理 其他都能看到
IList<User> superUsers = await _userManager.GetUsersInRoleAsync("Super Admin");
@ -408,7 +411,7 @@ namespace LMS.service.Service
query = query.Where(x => !filteredParentIds.Contains(x.UserID) || x.UserID == requestUserId);
}
}
else if (isAgent && !isSuperAdmin)
else if (isAgent)
{
// 代理只能看到自己下面的用户
var list = await userLookupQuery
@ -417,10 +420,18 @@ namespace LMS.service.Service
.ToListAsync();
HashSet<long> filteredParentIds = new(list);
if (filteredParentIds?.Count > 0)
if (filteredParentIds != null)
{
query = query.Where(x => filteredParentIds.Contains(x.UserID) || x.UserID == requestUserId);
}
else
{
query = query.Where(x => x.UserID == user.Id);
}
}
else
{
query = query.Where(x => x.UserID == user.Id);
}
}

View File

@ -291,7 +291,11 @@ namespace LMS.service.Service.SoftwareService
// 做筛选权限
// 获取相关用户ID
var userLookupQuery = _userManager.Users.AsNoTracking();
if (isAdmin && !isSuperAdmin)
if (isSuperAdmin)
{
}
else if (isAdmin)
{
// 除了超级管理员的代理 其他都能看到
IList<User> superUsers = await _userManager.GetUsersInRoleAsync("Super Admin");
@ -307,7 +311,7 @@ namespace LMS.service.Service.SoftwareService
query = query.Where(x => !filteredParentIds.Contains(x.UserId) || x.UserId == requestUserId);
}
}
else if (isAgent && !isSuperAdmin)
else if (isAgent)
{
// 代理只能看到自己下面的用户
var list = await userLookupQuery
@ -316,12 +320,16 @@ namespace LMS.service.Service.SoftwareService
.ToListAsync();
HashSet<long> filteredParentIds = new(list);
if (filteredParentIds?.Count > 0)
if (filteredParentIds != null)
{
query = query.Where(x => filteredParentIds.Contains(x.UserId));
}
else
{
query = query.Where(x => x.UserId == requestUserId);
}
}
else if (!isSuperAdmin)
else
{
// 普通用户只能看到自己的
query = query.Where(x => x.UserId == requestUserId);

View File

@ -1,4 +1,6 @@

using LMS.Common.Enum;
using LMS.Common.Password;
using LMS.Common.RSAKey;
using LMS.DAO;
using LMS.DAO.UserDAO;
@ -15,16 +17,18 @@ using System.Collections.Concurrent;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using static Betalgo.Ranul.OpenAI.ObjectModels.SharedModels.IOpenAIModels;
using static LMS.Common.Enums.ResponseCodeEnum;
namespace LMS.service.Service.UserService
{
public class LoginService(UserManager<User> userManager, ApplicationDbContext context, SecurityService securityService, UserBasicDao userBasicDao)
public class LoginService(UserManager<User> userManager, ApplicationDbContext context, SecurityService securityService, UserBasicDao userBasicDao, EmailVerificationService emailVerificationService)
{
private readonly UserManager<User> _userManager = userManager;
private readonly ApplicationDbContext _context = context;
private readonly SecurityService _securityService = securityService;
private readonly UserBasicDao _userBasicDao = userBasicDao;
private readonly EmailVerificationService _verificationService = emailVerificationService;
#region JWT
/// <summary>
@ -417,7 +421,8 @@ namespace LMS.service.Service.UserService
}
// 检查当前用户是不是超级管理员
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
// 不是超级管理员,不能重置其他用户的密码
if (!isSuperAdmin && id != requestUserId)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
@ -458,5 +463,97 @@ namespace LMS.service.Service.UserService
}
}
#endregion
#region
internal async Task<ActionResult<APIResponseModel<string>>> SendResetPasswordCode(EmailVerificationService.SendVerificationCodeDto model)
{
try
{
// 检查邮箱是否已被使用
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser == null)
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "当前邮箱未注册,请先注册!");
Options? enaleMailService = await _context.Options.FirstOrDefaultAsync(x => x.Key == OptionKeyName.EnableMailService);
if (enaleMailService != null)
{
_ = bool.TryParse(enaleMailService.Value, out bool enableMail);
if (!enableMail)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, "邮件服务未开启,无法重置密码!");
}
}
// 发送验证码
await _verificationService.SendResetPasswordCodeAsync(model.Email);
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, "验证码发送成功,请在邮箱中查收!");
}
catch (Exception e)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, e.Message);
}
}
#endregion
#region
public async Task<ActionResult<APIResponseModel<string>>> UserResetPassword(string mail, string code)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// 检查邮箱是否已被使用
var existingUser = await _userManager.FindByEmailAsync(mail);
if (existingUser == null)
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "当前邮箱未注册,请先注册!");
// 校验验证码
Options? enaleMailService = await _context.Options.FirstOrDefaultAsync(x => x.Key == OptionKeyName.EnableMailService);
if (enaleMailService != null)
{
_ = bool.TryParse(enaleMailService.Value, out bool enableMail);
if (!enableMail)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, "邮件服务未开启,无法重置密码!");
}
}
var isCodeValid = await _verificationService.VerifyResetPasswordAsync(mail, code);
if (!isCodeValid)
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "验证码无效或已过期");
// 生成一个密码
string randomPassword = PasswordGenerator.GeneratePassword(12);
// 移除用户当前密码(如果用户没有密码,则需要跳过此步骤)
var hasPassword = await _userManager.HasPasswordAsync(existingUser);
if (hasPassword)
{
var removePasswordResult = await _userManager.RemovePasswordAsync(existingUser);
if (!removePasswordResult.Succeeded)
{
var errors = string.Join("; ", removePasswordResult.Errors.Select(e => e.Description));
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, $"移除旧密码失败:{errors}");
}
}
// 为用户设置新密码
var addPasswordResult = await _userManager.AddPasswordAsync(existingUser, randomPassword);
if (!addPasswordResult.Succeeded)
{
var errors = string.Join("; ", addPasswordResult.Errors.Select(e => e.Description));
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, $"重置密码失败:{errors}");
}
await transaction.CommitAsync();
// 重置密码成功,把新密码发送到邮箱
await _verificationService.SendResetpasswordSuccessMail(mail, randomPassword);
// 重置密码
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, "密码重置成功,请去邮箱查看重置密码!");
}
catch (Exception e)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, e.Message);
}
}
#endregion
}
}

View File

@ -139,7 +139,11 @@ namespace LMS.service.Service.UserService
// 获取相关用户ID
var userLookupQuery = _userManager.Users.AsNoTracking();
if (isAdmin && !isSuperAdmin)
if (isSuperAdmin)
{
}
else if (isAdmin)
{
// 除了超级管理员的代理 其他都能看到
IList<User> superUsers = await _userManager.GetUsersInRoleAsync("Super Admin");
@ -155,7 +159,7 @@ namespace LMS.service.Service.UserService
query = query.Where(x => !filteredParentIds.Contains(x.Id));
}
}
else if (isAgent && !isSuperAdmin)
else if (isAgent)
{
// 代理只能看到自己下面的用户
var list = await userLookupQuery
@ -164,10 +168,18 @@ namespace LMS.service.Service.UserService
.ToListAsync();
HashSet<long> filteredParentIds = new(list);
if (filteredParentIds?.Count > 0)
if (filteredParentIds != null)
{
query = query.Where(x => filteredParentIds.Contains(x.Id));
}
else
{
query = query.Where(x => x.Id == reuqertUserId);
}
}
else
{
query = query.Where(x => x.Id == reuqertUserId);
}

View File

@ -26,6 +26,6 @@
],
"Enrich": [ "FromLogContext" ]
},
"Version": "1.0.4",
"Version": "1.0.5",
"AllowedHosts": "*"
}