From a23984c9f532c01117086ea812c1a0799d65fdb7 Mon Sep 17 00:00:00 2001
From: lq1405 <2769838458@qq.com>
Date: Sat, 22 Mar 2025 20:48:40 +0800
Subject: [PATCH] =?UTF-8?q?V1.0.5=20=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?=
=?UTF-8?q?=E9=87=8D=E7=BD=AE=E5=AF=86=E7=A0=81=E5=92=8C=E4=BF=AE=E6=94=B9?=
=?UTF-8?q?=E5=AF=86=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
LMS.Common/Password/RandomNumberGenerator.cs | 78 ++++++++++++++
LMS.Common/Templates/EmailTemplateService.cs | 31 +++++-
LMS.service/Controllers/UserController.cs | 46 +++++++-
.../Mail/EmailVerificationService.cs | 91 +++++++++++++++-
LMS.service/Service/MachineService.cs | 17 ++-
.../SoftwareService/SoftwareControlService.cs | 16 ++-
.../Service/UserService/LoginService.cs | 101 +++++++++++++++++-
.../Service/UserService/UserService.cs | 18 +++-
LMS.service/appsettings.json | 2 +-
9 files changed, 380 insertions(+), 20 deletions(-)
create mode 100644 LMS.Common/Password/RandomNumberGenerator.cs
diff --git a/LMS.Common/Password/RandomNumberGenerator.cs b/LMS.Common/Password/RandomNumberGenerator.cs
new file mode 100644
index 0000000..4df92ca
--- /dev/null
+++ b/LMS.Common/Password/RandomNumberGenerator.cs
@@ -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;
+
+ ///
+ /// 生成指定长度的随机密码
+ ///
+ /// 密码长度
+ /// 随机生成的密码
+ 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);
+ }
+
+ ///
+ /// 从指定字符集中获取一个随机字符
+ ///
+ private static char GetRandomChar(string chars, RandomNumberGenerator rng)
+ {
+ byte[] data = new byte[1];
+ rng.GetBytes(data);
+ return chars[data[0] % chars.Length];
+ }
+
+ ///
+ /// 打乱字符串中字符的顺序
+ ///
+ 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);
+ }
+ }
+}
diff --git a/LMS.Common/Templates/EmailTemplateService.cs b/LMS.Common/Templates/EmailTemplateService.cs
index a1d5fb6..ccc465d 100644
--- a/LMS.Common/Templates/EmailTemplateService.cs
+++ b/LMS.Common/Templates/EmailTemplateService.cs
@@ -9,14 +9,37 @@
- LMS Registration Code
- Hi there,
- Your code is {RegisterCode}
- The code is valid for 10 minutes, if it is not you, please ignore it.
+ LMS注册验证码
+ 您好,
+ 您的验证码是 {RegisterCode}
+ 该验证码有效期为 10 分钟,如果不是您本人操作,请忽略此邮件。
""";
+ public const string ResetPasswordHtmlTemplates = """
+
+
+
+ LMS重置密码验证码
+ 您好,
+ 您的验证码是 {ResetPasswordCode}
+ 该验证码有效期为 10 分钟,如果不是您本人操作,请忽略此邮件。
+
+
+ """;
+
+ public const string ResetpasswordSuccessMail = """
+
+
+
+ LMS重置密码成功
+ 您好,
+ 您的重置密码操作已经成功!
+ 您的新密码是 {NewPassword}
+
+
+ """;
///
/// 替换模板的占位符
diff --git a/LMS.service/Controllers/UserController.cs b/LMS.service/Controllers/UserController.cs
index 24a0cde..131698f 100644
--- a/LMS.service/Controllers/UserController.cs
+++ b/LMS.service/Controllers/UserController.cs
@@ -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>> SendResetPasswordCode([FromBody] EmailVerificationService.SendVerificationCodeDto model)
+ {
+ if (!ModelState.IsValid)
+ return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError);
+
+ return await _loginService.SendResetPasswordCode(model);
+ }
+
+ #endregion
+
+ #region 用户自行重置密码
+
+ [HttpPost("{mail}/{code}")]
+ public async Task>> ResetPassword(string mail, string code)
+ {
+ // 校验邮箱是不是有效
+ if (string.IsNullOrWhiteSpace(mail))
+ return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的邮箱");
+
+ try
+ {
+ // 尝试创建MailAddress实例,如果成功则表示邮箱格式正确
+ MailAddress mailAddress = new(mail);
+ bool isMail = mailAddress.Address == mail;
+ if (!isMail)
+ return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的邮箱");
+ return await _loginService.UserResetPassword(mail, code);
+
+ }
+ catch (FormatException)
+ {
+ // 创建失败表示邮箱格式不正确
+ return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的邮箱");
+ }
+
+ }
+
+ #endregion
+
#region 刷新token
[HttpPost]
@@ -256,7 +300,7 @@ namespace LMS.service.Controllers
#endregion
- #region 管理员重置用户密码
+ #region 重置用户密码
[HttpPost("{id}")]
[Authorize]
diff --git a/LMS.service/Extensions/Mail/EmailVerificationService.cs b/LMS.service/Extensions/Mail/EmailVerificationService.cs
index cb097db..cb82334 100644
--- a/LMS.service/Extensions/Mail/EmailVerificationService.cs
+++ b/LMS.service/Extensions/Mail/EmailVerificationService.cs
@@ -23,7 +23,13 @@ public class EmailVerificationService
public string Email { get; set; }
}
- // 生成并发送验证码
+ #region 注册验证码
+
+ ///
+ /// 发送注册验证码
+ ///
+ ///
+ ///
public async Task SendVerificationCodeAsync(string email)
{
// 生成6位数验证码
@@ -46,7 +52,12 @@ public class EmailVerificationService
await _emailService.SendEmailAsync(email, "LMS注册验证码", emailBody);
}
- // 验证用户提交的验证码
+ ///
+ /// 验证用户提交的验证码
+ ///
+ ///
+ ///
+ ///
public async Task VerifyCodeAsync(string email, string code)
{
var storedCode = await _cache.GetStringAsync($"EmailVerification_{email}");
@@ -64,8 +75,84 @@ public class EmailVerificationService
return false;
}
+ ///
+ /// 生成一个6位数的验证码
+ ///
+ ///
+
private string GenerateVerificationCode()
{
return _random.Next(100000, 999999).ToString();
}
+
+ #endregion
+
+ #region 重置密码验证码
+
+ ///
+ /// 发送重置密码验证码
+ ///
+ ///
+ ///
+ 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
+ {
+ { "ResetPasswordCode", resetPasswordCode }
+ });
+ // 发送验证码邮件
+ await _emailService.SendEmailAsync(email, "LMS重置密码验证码", emailBody);
+ }
+
+ ///
+ /// 严重用户提交成的重置密码验证码
+ ///
+ ///
+ ///
+ ///
+ public async Task 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 重置密码成功邮件
+
+ ///
+ /// 发送重置密码验证码
+ ///
+ ///
+ ///
+ public async Task SendResetpasswordSuccessMail(string email, string newPassword)
+ {
+ var emailBody = EmailTemplateService.ReplaceTemplate(EmailTemplateService.ResetpasswordSuccessMail, new Dictionary
+ {
+ { "NewPassword", newPassword }
+ });
+ // 发送验证码邮件
+ await _emailService.SendEmailAsync(email, "LMS重置密码成功", emailBody);
+ }
+
+ #endregion
}
\ No newline at end of file
diff --git a/LMS.service/Service/MachineService.cs b/LMS.service/Service/MachineService.cs
index 50f14c0..19cc4f0 100644
--- a/LMS.service/Service/MachineService.cs
+++ b/LMS.service/Service/MachineService.cs
@@ -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 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 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);
}
}
diff --git a/LMS.service/Service/SoftwareService/SoftwareControlService.cs b/LMS.service/Service/SoftwareService/SoftwareControlService.cs
index f3e26e8..cac3ead 100644
--- a/LMS.service/Service/SoftwareService/SoftwareControlService.cs
+++ b/LMS.service/Service/SoftwareService/SoftwareControlService.cs
@@ -291,7 +291,11 @@ namespace LMS.service.Service.SoftwareService
// 做筛选权限
// 获取相关用户ID
var userLookupQuery = _userManager.Users.AsNoTracking();
- if (isAdmin && !isSuperAdmin)
+ if (isSuperAdmin)
+ {
+
+ }
+ else if (isAdmin)
{
// 除了超级管理员的代理 其他都能看到
IList 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 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);
diff --git a/LMS.service/Service/UserService/LoginService.cs b/LMS.service/Service/UserService/LoginService.cs
index 92c6303..647b7db 100644
--- a/LMS.service/Service/UserService/LoginService.cs
+++ b/LMS.service/Service/UserService/LoginService.cs
@@ -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 userManager, ApplicationDbContext context, SecurityService securityService, UserBasicDao userBasicDao)
+ public class LoginService(UserManager userManager, ApplicationDbContext context, SecurityService securityService, UserBasicDao userBasicDao, EmailVerificationService emailVerificationService)
{
private readonly UserManager _userManager = userManager;
private readonly ApplicationDbContext _context = context;
private readonly SecurityService _securityService = securityService;
private readonly UserBasicDao _userBasicDao = userBasicDao;
+ private readonly EmailVerificationService _verificationService = emailVerificationService;
#region 生成JWT
///
@@ -417,7 +421,8 @@ namespace LMS.service.Service.UserService
}
// 检查当前用户是不是超级管理员
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
- if (!isSuperAdmin)
+ // 不是超级管理员,不能重置其他用户的密码
+ if (!isSuperAdmin && id != requestUserId)
{
return APIResponseModel.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
@@ -458,5 +463,97 @@ namespace LMS.service.Service.UserService
}
}
#endregion
+
+ #region 发送重置密码验证码
+ internal async Task>> SendResetPasswordCode(EmailVerificationService.SendVerificationCodeDto model)
+ {
+ try
+ {
+ // 检查邮箱是否已被使用
+ var existingUser = await _userManager.FindByEmailAsync(model.Email);
+ if (existingUser == null)
+ return APIResponseModel.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.CreateErrorResponseModel(ResponseCode.SystemError, "邮件服务未开启,无法重置密码!");
+ }
+ }
+
+ // 发送验证码
+ await _verificationService.SendResetPasswordCodeAsync(model.Email);
+ return APIResponseModel.CreateSuccessResponseModel(ResponseCode.Success, "验证码发送成功,请在邮箱中查收!");
+ }
+ catch (Exception e)
+ {
+ return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, e.Message);
+ }
+ }
+ #endregion
+ #region 用户自行重置密码
+ public async Task>> UserResetPassword(string mail, string code)
+ {
+ using var transaction = await _context.Database.BeginTransactionAsync();
+ try
+ {
+ // 检查邮箱是否已被使用
+ var existingUser = await _userManager.FindByEmailAsync(mail);
+ if (existingUser == null)
+ return APIResponseModel.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.CreateErrorResponseModel(ResponseCode.SystemError, "邮件服务未开启,无法重置密码!");
+ }
+ }
+
+ var isCodeValid = await _verificationService.VerifyResetPasswordAsync(mail, code);
+ if (!isCodeValid)
+ return APIResponseModel.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.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.CreateErrorResponseModel(ResponseCode.SystemError, $"重置密码失败:{errors}");
+ }
+ await transaction.CommitAsync();
+
+ // 重置密码成功,把新密码发送到邮箱
+ await _verificationService.SendResetpasswordSuccessMail(mail, randomPassword);
+
+ // 重置密码
+ return APIResponseModel.CreateSuccessResponseModel(ResponseCode.Success, "密码重置成功,请去邮箱查看重置密码!");
+ }
+ catch (Exception e)
+ {
+ return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, e.Message);
+ }
+ }
+ #endregion
}
}
diff --git a/LMS.service/Service/UserService/UserService.cs b/LMS.service/Service/UserService/UserService.cs
index 3ca6168..c8835b3 100644
--- a/LMS.service/Service/UserService/UserService.cs
+++ b/LMS.service/Service/UserService/UserService.cs
@@ -139,7 +139,11 @@ namespace LMS.service.Service.UserService
// 获取相关用户ID
var userLookupQuery = _userManager.Users.AsNoTracking();
- if (isAdmin && !isSuperAdmin)
+ if (isSuperAdmin)
+ {
+
+ }
+ else if (isAdmin)
{
// 除了超级管理员的代理 其他都能看到
IList 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 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);
}
diff --git a/LMS.service/appsettings.json b/LMS.service/appsettings.json
index d3a6715..9a3b867 100644
--- a/LMS.service/appsettings.json
+++ b/LMS.service/appsettings.json
@@ -26,6 +26,6 @@
],
"Enrich": [ "FromLogContext" ]
},
- "Version": "1.0.4",
+ "Version": "1.0.5",
"AllowedHosts": "*"
}