前言:
本系列文章主要為我之前所學知識的一次微小的實踐,以我學校圖書館管理系統為雛形所作。
本系列文章主要參考資料:
微軟文檔: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 的使用、配置文件的使用
一、添加密碼修改功能
首先創建對應的視圖模型:
其中 [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 }
在 StudentAccountController 中添加 [Authorize] 特性,然后可以去除 StudentAccountController 中方法的 [Authorize] 特性。當方法不需要授權即可訪問時添加 [AllowAnonymous] 特性。
1 [Authorize] 2 public class StudentAccountController : Controller
利用 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 Task<IActionResult> ModifyPassword(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 添加相應的事件。將<script></script>標簽統一放在 @section Scripts 以方便地使用布局:
1 @model ModifyModel
2
3 @{
4 ViewData["Title"] = "ModifyPassword";
5 }
6
7 @section Scripts{
8 <script>
9 $(document).ready(function() {
10 var $btn = $("#showPas");
11 var btn = $btn.get(0);
12 $btn.click(function() {
13 if (btn.checked) {
14 $(".pass").attr("type", "");
15 } else {
16 $(".pass").attr("type", "password");
17 }
18 });
19 })
20 </script>
21 }
22
23
24 <h2>修改密碼</h2>
25
26 <div class="text-danger" asp-validation-summary="All"></div>
27 <form asp-action="ModifyPassword" method="post">
28 <div class="form-group">
29 <label asp-for="OriginalPassword"></label>
30 <input asp-for="OriginalPassword" class="pass"/>
31 </div>
32 <div class="form-group">
33 <label asp-for="ModifiedPassword"></label>
34 <input asp-for="ModifiedPassword" id="modifiedPassword" class="pass"/>
35 </div>
36 <div class="form-group">
37 <label asp-for="ConfirmedPassword"></label>
38 <input asp-for="ConfirmedPassword" id="confirmedPassword" onkeydown="" class="pass"/>
39 </div>
40 <div class="form-group">
41 <label>顯示密碼 </label><input style="margin-left: 10px" type="checkbox" id="showPas"/>
42 </div>
43 <input type="submit"/>
44 <input type="reset"/>
45 </form>
隨便建的 ModifySuccess 視圖:
1 @{
2 ViewData["Title"] = "修改成功";
3 }
4
5 <h2>修改成功</h2>
6
7 <h4><a asp-action="Login">請重新登錄</a></h4>
然后修改 AccountInfo 視圖以添加對應的修改密碼的按鈕:
1 @model Dictionary<string, object>
2 @{
3 ViewData["Title"] = "AccountInfo";
4 }
5 <h2>賬戶信息</h2>
6 <ul>
7 @foreach (var info in Model)
8 {
9 <li>@info.Key: @Model[info.Key]</li>
10 }
11 </ul>
12 <br />
13 <a class="btn btn-danger" asp-action="Logout">登出</a>
14 <a class="btn btn-primary" asp-action="ModifyPassword">修改密碼</a>






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 }
這里請自行輸入自己的 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<EmailSender>();
創建用於確定找回途徑的模型:
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<Student> _userManager;
4 public EmailSender _emailSender;
5
6 public PasswordRetrieverController(UserManager<Student> 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 <h2>找回密碼</h2> 4 <hr/> 5 6 <label class="text-danger">@ViewBag.Error</label> 7 8 <form asp-action="RetrievePassword" asp-controller="PasswordRetriever" method="post"> 9 <div class="form-group"> 10 <input asp-for="Account" class="form-control" placeholder="請輸入你的郵箱 / 賬號 / 手機號"/> 11 </div> 12 <br/> 13 <div class="form-group"> 14 <label>找回方式</label> 15 <select asp-for="RetrieveWay"> 16 <option disabled value="">找回方式: </option> 17 <LoginType login-type="@Enum.GetNames(typeof(RetrieveType))"></LoginType> 18 </select> 19 </div> 20 <br/> 21 <input class="btn btn-primary" type="submit" value="確認"/> 22 <input class="btn btn-primary" type="reset"/> 23 </form>

定義用來進行具體邏輯驗證的 RetrievePassword 方法,該方法驗證用戶是否存在,生成用以重置密碼的 token 並發送郵件:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RetrievePassword(RetrieveModel model)
{
bool sendResult=false;
if (ModelState.IsValid)
{
Student student = new Student();
switch (model.RetrieveWay)
{
case RetrieveType.UserName:
student = await _userManager.FindByNameAsync(model.Account);
if (student != null)
{
string code = await _userManager.GeneratePasswordResetTokenAsync(student);
sendResult = await SendEmail(student.Id, code, student.Email);
}
break;
case RetrieveType.Email:
student = await _userManager.FindByEmailAsync(model.Account);
if (student != null)
{
string code = await _userManager.GeneratePasswordResetTokenAsync(student);
sendResult = await SendEmail(student.Id, code, student.Email);
}
break;
}
if (student == null)
{
ViewBag.Error("用戶不存在,請重新輸入");
return View("Retrieve",model);
}
}
ViewBag.Message = "已發送郵件至您的郵箱,請注意查收";
ViewBag.Failed = "信息發送失敗";
return View(sendResult);
}
在 PasswordRetrieverController 中定義用以發送郵件的方法,以 bool 為返回值以判斷郵件是否發送成功,此處 MailMessage 處的 from 參數請自行配置:
async Task<bool> SendEmail(string userId, string code, string mailAddress)
{
Student student = await _userManager.FindByIdAsync(userId);
if (student!=null)
{
string url = Url.Action("ResetPassword","PasswordRetriever",new{userId=userId,code=code}, Url.ActionContext.HttpContext.Request.Scheme);
StringBuilder sb = new StringBuilder();
sb.AppendLine($" 請點擊<a href=\"{url}\">此處</a>重置您的密碼");
MailMessage message = new MailMessage(from: "xxxx@163.com", to: mailAddress, subject: "重置密碼", body: sb.ToString());
message.BodyEncoding=Encoding.UTF8;
message.IsBodyHtml = true;
try
{
_emailSender.SmtpClient.Send(message);
}
catch (Exception e)
{
return false;
}
return true;
}
return false;
}
為 Url.Action 方法指定 protocol 參數以生成完整 url ,否則只會生成相對 url。
為使用該 token,創建專門用於重置密碼的模型,其中 Code 用來接收 GeneratePasswordResetTokenAsync 生成的 token,UserId 用來傳遞待重置用戶的 Id:
1 public class ResetPasswordModel
2 {
3 public string Code { get; set; }
4
5 public string UserId { get; set; }
6
7 [Required]
8 [Display(Name="密碼")]
9 [DataType(DataType.Password)]
10 public string Password { get; set; }
11
12 [Required]
13 [Display(Name = "確認密碼")]
14 [DataType(DataType.Password)]
15 [Compare("Password",ErrorMessage = "兩次密碼不匹配")]
16 public string ConfirmPassword { get; set; }
17 }
定義用來重置密碼的方法 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 <h2>重置密碼</h2>
7
8 <form asp-action="ResetPassword" method="post" asp-antiforgery="true">
9 <div class="form-group">
10 @Html.HiddenFor(m=>m.Code)
11 @Html.HiddenFor(m=>m.UserId)
12 <label asp-for="Password"></label>
13 <input asp-for="Password"/>
14 </div>
15 <div class="form-group">
16 <label asp-for="ConfirmPassword"></label>
17 <input asp-for="ConfirmPassword"/>
18 </div>
19 <input type="submit"/>
20 <input type="reset"/>
21 </form>
定義用以具體邏輯驗證的 ResetPassword 方法,UserManager<T> 對象的 ResetPasswordAsync 方法接收一個 T類型對象、一個 token 字符串以及密碼,返回 IdentityResult 對象:
1 [ValidateAntiForgeryToken]
2 [HttpPost]
3 public async Task<IActionResult> ResetPassword(ResetPasswordModel model)
4 {
5 if (ModelState.IsValid)
6 {
7 var user = _userManager.FindByIdAsync(model.UserId);
8 if (user!=null)
9 {
10 var result = await _userManager.ResetPasswordAsync(user.Result, model.Code, model.Password);
11 if (result.Succeeded)
12 {
13 return RedirectToAction(nameof(ResetSuccess));
14 }
15 }
16 }
17 return View(model);
18 }
隨便定義的 ResetSuccess 方法和視圖:
1 public IActionResult ResetSuccess()
2 {
3 return View();
4 }
1 @{
2 ViewData["Title"] = "ResetSuccess";
3 }
4
5 <h2>重置成功</h2>
6
7 <h3>點擊<a asp-action="Login" asp-controller="StudentAccount" target="_blank">此處</a>進行登錄</h3>
最后向 _LoginParitalView 添加找回密碼的按鈕:
1 @model LoginModel 2 3 <input type="hidden" name="returnUrl" value="@ViewBag.returnUrl"/> 4 <div class="form-group"> 5 <label asp-for="Account"></label> 6 <input asp-for="Account" class="form-control" placeholder="請輸入你的賬號(學號) / 郵箱 / 手機號"/> 7 </div> 8 <div class="form-group"> 9 <label asp-for="Password"></label> 10 <input asp-for="Password" class="form-control" placeholder="請輸入你的密碼"/> 11 </div> 12 <div class="form-group"> 13 <label>登錄方式</label> 14 <select asp-for="LoginType"> 15 <option disabled value="">登錄方式</option> 16 <LoginType login-type="@Enum.GetNames(typeof(LoginType))"></LoginType> 17 </select> 18 </div> 19 <input type="submit" class="btn btn-primary"/> 20 <input type="reset" class="btn btn-primary"/> 21 <a class="btn btn-success" asp-action="Retrieve" asp-controller="PasswordRetriever">找回密碼</a>





