使用登入、電子郵件確認和密碼重設建立安全的 ASP.NET MVC 5 Web 應用程式 (C#)

作者: Rick Anderson

本教學課程說明如何使用 ASP.NET 身分識別成員資格系統,建置具有電子郵件確認和密碼重設的 ASP.NET MVC 5 Web 應用程式。

如需使用 .NET Core 的本教學課程更新版本,請參閱ASP.NET Core 中的帳戶確認和密碼復原

建立 ASP.NET MVC 應用程式

從安裝並執行Visual Studio Express 2013 for WebVisual Studio 2013開始。 安裝Visual Studio 2013 Update 3或更高版本。

注意

警告:您必須安裝Visual Studio 2013 Update 3或更高版本,才能完成本教學課程。

  1. 建立新的 ASP.NET Web 專案,然後選取 MVC 範本。 Web Form也支援 ASP.NET 身分識別,因此您可以遵循 Web 表單應用程式中的類似步驟。
    顯示 [新增 A S P 點 Net 專案] 頁面的螢幕擷取畫面。已選取 M V C 範本,並醒目提示 [個別使用者帳戶]。

  2. 將預設驗證保留為 [個別使用者帳戶]。 如果您想要在 Azure 中裝載應用程式,請保留核取方塊。 稍後在本教學課程中,我們將部署至 Azure。 您可以 免費開啟 Azure 帳戶

  3. 專案設定為使用 SSL

  4. 執行應用程式,按一下 [ 註冊 ] 連結並註冊使用者。 此時,電子郵件的唯一驗證是 [ EmailAddress] 屬性。

  5. 在 [伺服器總管] 中,流覽至 [資料連線\DefaultConnection\Tables\AspNetUsers],以滑鼠右鍵按一下並選取 [ 開啟資料表定義]。

    下圖顯示 AspNetUsers 架構:

    顯示 [伺服器總管] 中 [S P Net Users 腳本檔案] 索引標籤的螢幕擷取畫面。

  6. 以滑鼠右鍵按一下 [AspNetUsers ] 資料表,然後選取 [ 顯示資料表資料]。
    顯示 S P Net Users 架構的螢幕擷取畫面。已醒目提示標示為 False 的Email確認資料行。
    此時尚未確認電子郵件。

  7. 按一下資料列,然後選取 [刪除]。 您將在下一個步驟中再次新增此電子郵件,並傳送確認電子郵件。

Email確認

最佳做法是確認新使用者註冊的電子郵件,以確認他們未模擬其他人 (亦即,他們尚未向其他人的電子郵件註冊) 。 假設您有討論論壇,建議您防止 "bob@example.com" 註冊為 "joe@contoso.com" 。 如果沒有電子郵件確認, "joe@contoso.com" 可能會從您的應用程式收到不必要的電子郵件。 假設 Bob 不小心註冊為 "bib@example.com" ,但未注意到,他將無法使用密碼復原,因為應用程式沒有正確的電子郵件。 Email確認僅提供來自 Bot 的有限保護,且不提供來自已決定垃圾郵件者的保護,他們有許多可用來註冊的工作電子郵件別名。

您通常想要防止新使用者在電子郵件、簡訊或其他機制確認之前,將任何資料張貼到您的網站。 在下列各節中,我們將啟用電子郵件確認並修改程式碼,以防止新註冊的使用者登入,直到確認其電子郵件為止。

連結 SendGrid

本節中的指示不是最新的。 如需更新的指示 ,請參閱設定 SendGrid 電子郵件提供者

雖然本教學課程只會示範如何透過 SendGrid新增電子郵件通知,但您可以使用 SMTP 和其他機制傳送電子郵件, (請參閱 其他資源) 。

  1. 在 [套件管理器主控台] 中,輸入下列命令:

    Install-Package SendGrid
    
  2. 移至 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>

執行應用程式,然後按一下 [註冊] 連結。 提交註冊表單之後,即會登入。

顯示 [我的 A S P 點 NET 登入首頁] 的螢幕擷取畫面。

請檢查您的電子郵件帳戶,然後按一下連結以確認您的電子郵件。

登入前需要電子郵件確認

使用者目前完成註冊表單之後,就會登入。 您通常想要先確認其電子郵件,再將其登入。 在下列章節中,我們會修改程式碼,要求新使用者先確認電子郵件,再登入 (驗證) 。 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);
}

ForgotPasswordViews\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 中,會先建立為本機登入,但您可以先將帳戶建立為社交記錄,然後再新增本機登入。

顯示 [我的 A S P 點 Net Log In 首頁] 的螢幕擷取畫面。已醒目提示範例使用者識別碼。

按一下 [ 管理] 連結。 請注意 外部登入:0 與此帳戶相關聯。

顯示 [我的 A S P 點 Net 管理您的帳戶] 頁面的螢幕擷取畫面。在 [外部登入] 行旁,反白顯示 [0] 和 [管理] 連結。

按一下另一個登入服務的連結,並接受應用程式要求。 這兩個帳戶已合併,您將能夠使用任一帳戶登入。 您可能會想要讓使用者新增本機帳戶,以防其社交記錄驗證服務已關閉,或者他們可能失去其社交帳戶的存取權。

在下圖中,Tom 是 (中的社交記錄 ,您可以從外部登入看到:1 顯示在頁面上) 。

顯示 [我的 A S P 點 Net 管理您的帳戶] 頁面的螢幕擷取畫面。[挑選密碼] 和 [外部登入] 行會反白顯示。

按一下 [ 挑選密碼 ] 可讓您新增與相同帳戶相關聯的本機登入。

顯示 [我的 A S P 點 Net 建立本機登入] 頁面的螢幕擷取畫面。範例密碼會在 [新增密碼] 和 [確認新的密碼文字] 欄位中輸入。

更深入地Email確認

我的教學課程 帳戶確認和密碼復原與 ASP.NET 身分識別 會進一步詳細說明本主題。

偵錯應用程式

如果您沒有收到包含連結的電子郵件:

  • 檢查您的垃圾郵件或垃圾郵件資料夾。
  • 登入您的 SendGrid 帳戶,然後按一下[Email活動] 連結

若要測試沒有電子郵件的驗證連結,請下載 已完成的範例。 確認連結和確認碼會顯示在頁面上。

其他資源