前言:
本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。
本系列文章主要参考资料:
微软文档:https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-2.1&tabs=windows
《Pro ASP.NET MVC 5》、《锋利的 jQuery》
此系列皆使用 VS2017+C# 作为开发环境。如果有什么问题或者意见欢迎在留言区进行留言。
项目 github 地址:https://github.com/NanaseRuri/LibraryDemo
本章内容:Identity 修改密码和找回密码、c# SMTP 的使用、配置文件的使用
一、添加密码修改功能
首先创建对应的视图模型:
其中 16 行的 [Compare] 特性构造函数参数为需进行对比的属性,此处用于确认修改后的密码。
1 public class ModifyModel 2 { 3 [UIHint("password")] 4 [Display(Name = "原密码")] 5 [Required] 6 public string OriginalPassword { get; set; } 7 8 [Required] 9 [Display(Name = "新密码")] 10 [UIHint("password")] 11 public string ModifiedPassword { get; set; } 12 13 [Required] 14 [Display(Name = "确认密码")] 15 [UIHint("password")] 16 [Compare("ModifiedPassword", ErrorMessage = "两次密码不匹配")] 17 public string ConfirmedPassword { get; set; } 18 }
利用 Identity 框架中 UserManager 对象的 ChangePasswordAsync 方法用来修改密码,该方法返回一个 IdentityResult 对象,可通过其 Succeeded 属性查看操作是否成功。在此修改成功后调用 _signInManager.SignOutAsync() 方法来清除当前 Cookie。
定义用于修改密码的动作方法和视图:
1 public IActionResult ModifyPassword() 2 { 3 ModifyModel model=new ModifyModel(); 4 return View(model); 5 } 6 7 [HttpPost] 8 [ValidateAntiForgeryToken] 9 public async TaskModifyPassword(ModifyModel model) 10 { 11 if (ModelState.IsValid) 12 { 13 string username = HttpContext.User.Identity.Name; 14 var student = _userManager.Users.FirstOrDefault(s => s.UserName == username); 15 var result = 16 await _userManager.ChangePasswordAsync(student, model.OriginalPassword, model.ModifiedPassword); 17 if (result.Succeeded) 18 { 19 await _signInManager.SignOutAsync(); 20 return View("ModifySuccess"); 21 } 22 ModelState.AddModelError("","原密码输入错误"); 23 } 24 return View(model); 25 }
ModifyPassword 视图,添加用以表示是否显示密码的复选框,并使用 jQuery 和 JS 添加相应的事件。将标签统一放在 @section Scripts 以方便地使用布局:
1 @model ModifyModel 2 3 @{ 4 ViewData["Title"] = "ModifyPassword"; 5 } 6 7 @section Scripts{ 8 21 } 22 23 24修改密码
25 26class="text-danger" asp-validation-summary="All">27
随便建的 ModifySuccess 视图:
1 @{ 2 ViewData["Title"] = "修改成功"; 3 } 4 5修改成功
6 7"Login">请重新登录
然后修改 AccountInfo 视图以添加对应的修改密码的按钮:
1 @model Dictionary<string, object> 2 @{ 3 ViewData["Title"] = "AccountInfo"; 4 } 5账户信息
6
-
7 @foreach (var info in Model)
8 {
9
- @info.Key: @Model[info.Key] 10 } 11
13 class="btn btn-danger" asp-action="Logout">登出 14 class="btn btn-primary" asp-action="ModifyPassword">修改密码
Cookie 被清除:
二、重置密码
在 Identity 框架中, UserManager 提供了 GeneratePasswordResetTokenAsync 以及 ResetPasswordAsync 方法用以重置密码。
现实生活中,一般通过邮件发送重置连接来重置密码,为日后更方便地配置,在此创建 Mail.json
1 { 2 "Mail": { 3 "MailFromAddress": "", 4 "UseSsl": "false", 5 "Username": "", 6 "Password": "", 7 "ServerPort": "25", 8 "ServerName": "smtp.163.com", 9 "UseDefaultCredentials": "true" 10 } 11 }
各大邮箱运营商拥有自己的 SMTP 服务器,需要对应邮箱的请自行百度。这里仅展示 163 邮箱,这里请自行输入自己的 163 账号和密码。
然后创建一个类用来配置发送邮件的相关信息:
1 public class EmailSender 2 { 3 IConfiguration emailConfig = new ConfigurationBuilder().AddJsonFile("Mail.json").Build().GetSection("Mail"); 4 public SmtpClient SmtpClient=new SmtpClient(); 5 6 public EmailSender() 7 { 8 SmtpClient.EnableSsl = Boolean.Parse(emailConfig["UseSsl"]); 9 SmtpClient.UseDefaultCredentials = bool.Parse(emailConfig["UseDefaultCredentials"]); 10 SmtpClient.Credentials = new NetworkCredential(emailConfig["Username"], emailConfig["Password"]); 11 SmtpClient.Port = Int32.Parse(emailConfig["ServerPort"]); 12 SmtpClient.Host = emailConfig["ServerName"]; 13 SmtpClient.DeliveryMethod = SmtpDeliveryMethod.Network; 14 } 15 }
该类定义了一个读取配置的字段,以及一个用来发送邮件的 SmtpClient 属性。
此处第三行将会从 bin 文件夹中读取 Mail.json 文件中的 Mail 节点,为使 ConfigurationBuilder 能够读取到 bin 文件夹的文件,需要将 Mail.json 设置为复制到输出目录中:
然后该类将在构造函数对 SmtpClient 进行相应的配置。注意需要在为 SmtpClient 的 Credentials 属性赋值前为 UseDefaultCredentials 赋值,否则 Credentials 将被赋值为空值而出 Bug。
为使整个网页应用在整个生命期内使用的是同一个 SmtpClient 实例,在 ConfigureServices 中进行配置:
1 services.AddSingleton();
创建用于确定找回途径的模型:
1 public enum RetrieveType 2 { 3 UserName, 4 Email 5 } 6 7 public class RetrieveModel 8 { 9 [Required] 10 public RetrieveType RetrieveWay { get;set; } 11 [Required] 12 public string Account { get; set; } 13 }
定义一个 PasswordRetrieverController 专门用以处理找回密码的逻辑,Retrieve 方法创建接收用户信息输入的视图:
1 public class PasswordRetrieverController : Controller 2 { 3 private UserManager_userManager; 4 public EmailSender _emailSender; 5 6 public PasswordRetrieverController(UserManager studentManager, EmailSender emailSender) 7 { 8 _userManager = studentManager; 9 _emailSender = emailSender; 10 } 11 12 public IActionResult Retrieve() 13 { 14 RetrieveModel model = new RetrieveModel(); 15 return View(model); 16 }
Retrieve 视图:
1 @model RetrieveModel 2 3找回密码
4
5 6 7 8
定义用来进行具体逻辑验证的 RetrievePassword 方法,该方法验证用户是否存在,生成用以重置密码的 token 并发送邮件:
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public async TaskRetrievePassword(RetrieveModel model) 4 { 5 bool sendResult=false; 6 if (ModelState.IsValid) 7 { 8 Student student = new Student(); 9 switch (model.RetrieveWay) 10 { 11 case RetrieveType.UserName: 12 student = await _userManager.FindByNameAsync(model.Account); 13 if (student != null) 14 { 15 string code = await _userManager.GeneratePasswordResetTokenAsync(student); 16 sendResult = await SendEmail(student.Id, code, student.Email); 17 } 18 break; 19 case RetrieveType.Email: 20 student = await _userManager.FindByEmailAsync(model.Account); 21 if (student != null) 22 { 23 string code = await _userManager.GeneratePasswordResetTokenAsync(student); 24 sendResult = await SendEmail(student.Id, code, student.Email); 25 } 26 break; 27 } 28 if (student == null) 29 { 30 ViewBag.Error("用户不存在,请重新输入"); 31 return View("Retrieve",model); 32 } 33 } 34 ViewBag.Message = "已发送邮件至您的邮箱,请注意查收"; 35 ViewBag.Failed = "信息发送失败"; 36 return View(sendResult); 37 }
在 PasswordRetrieverController 中定义用以发送邮件的方法,以 bool 为返回值以判断邮件是否发送成功,此处 MailMessage 处的 from 参数请自行配置:
1 async Task<bool> SendEmail(string userId, string code, string mailAddress) 2 { 3 Student student = await _userManager.FindByIdAsync(userId); 4 if (student!=null) 5 { 6 string url = Url.Action("ResetPassword","PasswordRetriever",new{userId=userId,code=code}, Url.ActionContext.HttpContext.Request.Scheme); 7 StringBuilder sb = new StringBuilder(); 8 sb.AppendLine($" 请点击此处重置您的密码"); 9 MailMessage message = new MailMessage(from: "[email protected]", to: mailAddress, subject: "重置密码", body: sb.ToString()); 10 message.BodyEncoding=Encoding.UTF8; 11 message.IsBodyHtml = true; 12 try 13 { 14 _emailSender.SmtpClient.Send(message); 15 } 16 catch (Exception e) 17 { 18 return false; 19 } 20 21 return true; 22 } 23 return false; 24 }
为 Url.Action 方法指定 protocol 参数以生成完整 url ,否则只会生成相对 url,由于此处为发送邮件,所以需要指定 url 为绝对 Url。
为使用该 token,创建专门用于重置密码的模型,其中 Code 用来接收 GeneratePasswordResetTokenAsync 生成的 token,UserId 用来传递待重置用户的 Id:
public class ResetPasswordModel { public string Code { get; set; } public string UserId { get; set; } [Required] [Display(Name="密码")] [DataType(DataType.Password)] public string Password { get; set; } [Required] [Display(Name = "确认密码")] [DataType(DataType.Password)] [Compare("Password",ErrorMessage = "两次密码不匹配")] public string ConfirmPassword { get; set; } }
定义用来重置密码的方法 ResetPassword:
1 public IActionResult ResetPassword(string userId,string code) 2 { 3 ResetPasswordModel model=new ResetPasswordModel() 4 { 5 UserId = userId, 6 Code = code 7 }; 8 return View(model); 9 }
ResetPassword 视图,此视图将 token 和userId 设置为隐藏字段以在请求中传递:
1 @model ResetPasswordModel 2 @{ 3 ViewData["Title"] = "ResetPassword"; 4 } 5 6重置密码
7 8
定义用以具体逻辑验证的 ResetPassword 方法,UserManager
[ValidateAntiForgeryToken] [HttpPost] public async TaskResetPassword(ResetPasswordModel model) { if (ModelState.IsValid) { var user = _userManager.FindByIdAsync(model.UserId); if (user!=null) { var result = await _userManager.ResetPasswordAsync(user.Result, model.Code, model.Password); if (result.Succeeded) { return RedirectToAction(nameof(ResetSuccess)); } } } return View(model); }
随便定义的用以表示更改成功的 ResetSuccess 方法和视图:
1 public IActionResult ResetSuccess() 2 { 3 return View(); 4 }
1 @{ 2 ViewData["Title"] = "ResetSuccess"; 3 } 4 5重置成功
6 7点击"Login" asp-controller="StudentAccount" target="_blank">此处进行登录
最后向之前建立的 _LoginParitalView 视图中添加找回密码的按钮:
1 @model LoginModel 2 3 "hidden" name="returnUrl" value="@ViewBag.returnUrl"/> 4class="form-group"> 5 6 for="Account" class="form-control" placeholder="请输入你的账号(学号) / 邮箱 / 手机号"/> 78class="form-group"> 9 10 for="Password" class="form-control" placeholder="请输入你的密码"/> 1112class="form-group"> 13 14 <select asp-for="LoginType"> 15 1619 "submit" class="btn btn-primary"/> 20 "reset" class="btn btn-primary"/> 21 class="btn btn-success" asp-action="Retrieve" asp-controller="PasswordRetriever">找回密码"@Enum.GetNames(typeof(LoginType))"> 17 select> 18