使用登入、電子郵件確認和密碼重設建立安全的 ASP.NET MVC 5 Web 應用程式 (C#)
本教學課程說明如何使用 ASP.NET 身分識別成員資格系統,使用電子郵件確認和密碼重設,建置 ASP.NET MVC 5 Web 應用程式。
如需使用 .NET Core 之本教學課程的更新版本,請參閱 ASP.NET Core 中的帳戶確認和密碼復原。
建立 ASP.NET MVC 應用程式
從安裝並執行 Visual Studio Express 2013 for Web 或 Visual Studio 2013 開始。 安裝 Visual Studio 2013 Update 3 或更高版本。
注意
警告:您必須安裝 Visual Studio 2013 Update 3 或更高版本,才能完成本教學課程。
建立新的 ASP.NET Web 專案,然後選取 MVC 範本。 Web Form 也支援 ASP.NET 身分識別,因此您可以遵循 Web 窗體應用程式中的類似步驟。
將預設驗證保留為 個別用戶帳戶。 如果您想要在 Azure 中裝載應用程式,請保留複選框。 稍後在本教學課程中,我們將部署至 Azure。 您可以 免費開啟 Azure 帳戶。
將 項目設定為使用 SSL。
執行應用程式,按兩下 [註冊 ] 連結並註冊使用者。 此時,電子郵件上唯一的驗證是具有 [EmailAddress] 屬性。
在 [伺服器總管] 中,流覽至 [數據連線\DefaultConnection\Tables\AspNetUsers],以滑鼠右鍵按兩下並選取 [ 開啟數據表定義]。
下圖顯示
AspNetUsers
架構:以滑鼠右鍵按兩下 AspNetUsers 資料表,然後選取 [ 顯示資料表數據]。
此時,尚未確認電子郵件。按兩下資料列,然後選取 [刪除]。 您將在下一個步驟中再次新增此電子郵件,並傳送確認電子郵件。
電子郵件確認
最佳做法是確認新用戶註冊的電子郵件,以確認他們不會模擬其他人(也就是說,他們尚未向其他人的電子郵件註冊)。 假設您有討論論壇,建議您防止 "bob@example.com"
註冊為 "joe@contoso.com"
。 如果沒有電子郵件確認, "joe@contoso.com"
可能會從您的應用程式取得不必要的電子郵件。 假設 Bob 不小心註冊為 "bib@example.com"
,但沒注意到,他將無法使用密碼復原,因為應用程式沒有正確的電子郵件。 電子郵件確認只提供來自 Bot 的有限保護,而且不提供來自已決定垃圾郵件者的保護,因此有許多可用來註冊的工作電子郵件別名。
您通常想要防止新使用者在透過電子郵件、簡訊簡訊或其他機制確認之前,將任何數據張貼到您的網站。 在下列各節中,我們將啟用電子郵件確認並修改程序代碼,以防止新註冊的使用者登入,直到確認其電子郵件為止。
連結 SendGrid
本節中的指示不是最新的。 如需更新的指示,請參閱 設定 SendGrid 電子郵件提供者 。
雖然本教學課程只示範如何透過 SendGrid 新增電子郵件通知,但您可以使用 SMTP 和其他機制傳送電子郵件(請參閱 其他資源)。
在 [套件管理器主控台] 中,輸入下列命令:
Install-Package SendGrid
移至 Azure SendGrid 註冊頁面 ,並註冊免費的 SendGrid 帳戶。 在 App_Start/IdentityConfig.cs 中新增類似下列的程序代碼,以設定 SendGrid:
public class EmailService : IIdentityMessageService { public async Task SendAsync(IdentityMessage message) { await configSendGridasync(message); } // Use NuGet to install SendGrid (Basic C# client lib) private async Task configSendGridasync(IdentityMessage message) { var myMessage = new SendGridMessage(); myMessage.AddTo(message.Destination); myMessage.From = new System.Net.Mail.MailAddress( "Joe@contoso.com", "Joe S."); myMessage.Subject = message.Subject; myMessage.Text = message.Body; myMessage.Html = message.Body; var credentials = new NetworkCredential( ConfigurationManager.AppSettings["mailAccount"], ConfigurationManager.AppSettings["mailPassword"] ); // Create a Web transport for sending email. var transportWeb = new Web(credentials); // Send the email. if (transportWeb != null) { await transportWeb.DeliverAsync(myMessage); } else { Trace.TraceError("Failed to create Web transport."); await Task.FromResult(0); } } }
您必須新增下列專案:
using SendGrid;
using System.Net;
using System.Configuration;
using System.Diagnostics;
為了簡化此範例,我們會將應用程式設定儲存在 web.config 檔案中:
</connectionStrings>
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<!-- Markup removed for clarity. -->
<add key="mailAccount" value="xyz" />
<add key="mailPassword" value="password" />
</appSettings>
<system.web>
警告
安全性 - 永遠不要將敏感數據儲存在您的原始程式碼中。 帳戶和認證會儲存在 appSetting 中。 在 Azure 上,您可以在 Azure 入口網站 的 [設定] 索引標籤上安全地儲存這些值。 請參閱 將密碼和其他敏感數據部署至 ASP.NET 和 Azure 的最佳做法。
在帳戶控制站中啟用電子郵件確認
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account",
new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id,
"Confirm your account", "Please confirm your account by clicking <a href=\""
+ callbackUrl + "\">here</a>");
return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
確認 Views\Account\ConfirmEmail.cshtml 檔案具有正確的 razor 語法。 (第一行中的 @ 字元可能遺失。
@{
ViewBag.Title = "Confirm Email";
}
<h2>@ViewBag.Title.</h2>
<div>
<p>
Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
</p>
</div>
執行應用程式,然後按兩下 [註冊] 連結。 提交註冊窗體之後,就會登入。
請檢查您的電子郵件帳戶,然後按下連結以確認您的電子郵件。
登入前需要電子郵件確認
使用者目前完成註冊窗體之後,就會登入。 您通常會想要在登入電子郵件之前先確認電子郵件。 在下一節中,我們將修改程序代碼,要求新使用者在登入之前擁有已確認的電子郵件(已驗證)。 HttpPost Register
使用下列醒目提示的變更來更新 方法:
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
// Comment the following line to prevent log in until the user is confirmed.
// await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account",
new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Confirm your account",
"Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");
// Uncomment to debug locally
// TempData["ViewBagLink"] = callbackUrl;
ViewBag.Message = "Check your email and confirm your account, you must be confirmed "
+ "before you can log in.";
return View("Info");
//return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
藉由批注化 SignInAsync
方法,使用者將不會透過註冊登入。 這 TempData["ViewBagLink"] = callbackUrl;
一行可用來偵 錯應用程式和 測試註冊,而不傳送電子郵件。 ViewBag.Message
是用來顯示確認指示。 下載 範例 包含程式代碼,用來測試電子郵件確認而不設定電子郵件,也可用來偵錯應用程式。
建立檔案 Views\Shared\Info.cshtml
並新增下列 razor 標記:
@{
ViewBag.Title = "Info";
}
<h2>@ViewBag.Title.</h2>
<h3>@ViewBag.Message</h3>
將 Authorize 屬性 新增至 Contact
Home 控制器的 action 方法。 您可以按兩下 [ 連絡人 ] 連結,確認匿名使用者沒有存取權,且已驗證的用戶確實具有存取權。
[Authorize]
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
您也必須更新 HttpPost Login
動作方法:
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Require the user to have a confirmed email before they can log on.
var user = await UserManager.FindByNameAsync(model.Email);
if (user != null)
{
if (!await UserManager.IsEmailConfirmedAsync(user.Id))
{
ViewBag.errorMessage = "You must have a confirmed email to log on.";
return View("Error");
}
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
更新 Views\Shared\Error.cshtml 檢視以顯示錯誤訊息:
@model System.Web.Mvc.HandleErrorInfo
@{
ViewBag.Title = "Error";
}
<h1 class="text-danger">Error.</h1>
@{
if (String.IsNullOrEmpty(ViewBag.errorMessage))
{
<h2 class="text-danger">An error occurred while processing your request.</h2>
}
else
{
<h2 class="text-danger">@ViewBag.errorMessage</h2>
}
}
刪除 AspNetUsers 資料表中包含您想要測試之電子郵件別名的任何帳戶。 執行應用程式,並確認您必須先確認電子郵件位址,才能登入。 確認電子郵件地址之後,請按兩下 [ 連絡人] 連結。
密碼復原/重設
從 HttpPost ForgotPassword
帳號控制器中的動作方法中移除註出字元:
//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
{
// Don't reveal that the user does not exist or is not confirmed
return View("ForgotPasswordConfirmation");
}
string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>");
return RedirectToAction("ForgotPasswordConfirmation", "Account");
}
// If we got this far, something failed, redisplay form
return View(model);
}
從 ForgotPassword
Views\Account\Login.cshtml razor 檢視檔案中的 ActionLink 移除批注字元:
@using MvcPWy.Models
@model LoginViewModel
@{
ViewBag.Title = "Log in";
}
<h2>@ViewBag.Title.</h2>
<div class="row">
<div class="col-md-8">
<section id="loginForm">
@using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<h4>Use a local account to log in.</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
@Html.CheckBoxFor(m => m.RememberMe)
@Html.LabelFor(m => m.RememberMe)
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Log in" class="btn btn-default" />
</div>
</div>
<p>
@Html.ActionLink("Register as a new user", "Register")
</p>
@* Enable this once you have account confirmation enabled for password reset functionality *@
<p>
@Html.ActionLink("Forgot your password?", "ForgotPassword")
</p>
}
</section>
</div>
<div class="col-md-4">
<section id="socialLoginForm">
@Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl })
</section>
</div>
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
[登入] 頁面現在會顯示重設密碼的連結。
重新傳送電子郵件確認連結
一旦使用者建立新的本機帳戶,他們就會以電子郵件傳送確認連結,要求他們才能登入。 如果使用者不小心刪除確認電子郵件,或電子郵件永遠不會送達,他們將需要再次傳送確認連結。 下列程式代碼變更示範如何啟用這項功能。
將下列協助程式方法新增至 Controllers\AccountController.cs 檔案底部:
private async Task<string> SendEmailConfirmationTokenAsync(string userID, string subject)
{
string code = await UserManager.GenerateEmailConfirmationTokenAsync(userID);
var callbackUrl = Url.Action("ConfirmEmail", "Account",
new { userId = userID, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(userID, subject,
"Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");
return callbackUrl;
}
更新 Register 方法以使用新的協助程式:
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
// Comment the following line to prevent log in until the user is confirmed.
// await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account");
ViewBag.Message = "Check your email and confirm your account, you must be confirmed "
+ "before you can log in.";
return View("Info");
//return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
如果尚未確認使用者帳戶,請更新 Login 方法以重新傳送密碼:
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Require the user to have a confirmed email before they can log on.
// var user = await UserManager.FindByNameAsync(model.Email);
var user = UserManager.Find(model.Email, model.Password);
if (user != null)
{
if (!await UserManager.IsEmailConfirmedAsync(user.Id))
{
string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account-Resend");
// Uncomment to debug locally
// ViewBag.Link = callbackUrl;
ViewBag.errorMessage = "You must have a confirmed email to log on. "
+ "The confirmation token has been resent to your email account.";
return View("Error");
}
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
結合社交和本機登入帳戶
您可以按一下電子郵件連結來合併本機和社交帳戶。 在下列順序 RickAndMSFT@gmail.com 中,會先建立為本機登入,但您可以先建立帳戶作為社交記錄,然後新增本機登入。
按一下[管理]連結。 請注意外部 登入:0 與此帳戶相關聯。
按兩下另一個登入服務的連結,並接受應用程式要求。 這兩個帳戶已合併,您將能夠使用任一帳戶登入。 您可能會想要讓使用者新增本機帳戶,以防其社交登入驗證服務已關閉,或更有可能失去其社交帳戶的存取權。
在下圖中,Tom 是社交記錄檔(您可以從頁面顯示的 [外部登入:1] 中看到。
按兩下 [ 挑選密碼 ] 可讓您新增與相同帳戶相關聯的本機登入。
更深入的電子郵件確認
我的教學課程 帳戶確認和密碼復原與 ASP.NET 身分 識別會進入本主題,並提供更多詳細數據。
偵錯應用程式
如果您沒有收到包含連結的電子郵件:
- 檢查您的垃圾郵件資料夾。
- 登入您的 SendGrid 帳戶,然後按兩下 [ 電子郵件活動] 連結。
若要測試沒有電子郵件的驗證連結,請下載已完成的 範例。 確認連結和確認碼會顯示在頁面上。
其他資源
- ASP.NET 身分識別建議資源的連結
- 使用 ASP.NET 身分 識別進行帳戶確認和密碼復原,深入瞭解密碼復原和帳戶確認。
- 使用 Facebook、Twitter、LinkedIn 和 Google OAuth2 登入 的 MVC 5 應用程式本教學課程說明如何使用 Facebook 和 Google OAuth 2 授權撰寫 ASP.NET MVC 5 應用程式。 它也會示範如何將其他數據新增至身分識別資料庫。
- 將具有成員資格、OAuth 和 SQL 資料庫 的安全 ASP.NET MVC 應用程式部署至 Azure。 本教學課程會新增 Azure 部署、如何使用角色保護應用程式、如何使用成員資格 API 來新增使用者和角色,以及其他安全性功能。
- 建立適用於 OAuth 2 的 Google 應用程式,並將應用程式連線至專案
- 在 Facebook 中建立應用程式並將應用程式連線至專案
- 在項目中設定 SSL