Создание безопасного веб-приложения ASP.NET MVC 5 с входом, подтверждением электронной почты и сбросом пароля (C#)

Рик Андерсон

В этом руководстве показано, как создать веб-приложение ASP.NET MVC 5 с подтверждением электронной почты и сбросом пароля с помощью системы членства ASP.NET Identity.

Обновленную версию этого руководства, в котором используется .NET Core, см. в статье Подтверждение учетной записи и восстановление пароля в ASP.NET Core.

Создание приложения ASP.NET MVC

Начните с установки и запуска Visual Studio Express 2013 для Web или Visual Studio 2013. Установите Visual Studio 2013 с обновлением 3 или более поздней версии.

Примечание

Предупреждение. Для работы с этим руководством необходимо установить Visual Studio 2013 обновление 3 или более поздней версии.

  1. Создайте веб-проект ASP.NET и выберите шаблон MVC. веб-формы также поддерживает ASP.NET Identity, поэтому вы можете выполнить аналогичные действия в приложении веб-форм.
    Снимок экрана, на котором показана страница New A S P dot Net Project (Новый сетевой проект). Выбран шаблон M V C и выделены отдельные учетные записи пользователей.

  2. Оставьте для проверки подлинности по умолчанию значение Индивидуальные учетные записи пользователей. Если вы хотите разместить приложение в Azure, оставьте флажок проверка. Далее в этом руководстве мы развернем его в Azure. Вы можете открыть учетную запись Azure бесплатно.

  3. Задайте для проекта использование SSL.

  4. Запустите приложение, щелкните ссылку Зарегистрировать и зарегистрируйте пользователя. На этом этапе единственная проверка сообщения электронной почты выполняется с помощью атрибута [EmailAddress] .

  5. В Обозреватель сервера перейдите в раздел Подключения к данным\DefaultConnection\Tables\AspNetUsers, щелкните правой кнопкой мыши и выберите Открыть определение таблицы.

    На следующем рисунке показана AspNetUsers схема:

    Снимок экрана, на котором показана вкладка

  6. Щелкните правой кнопкой мыши таблицу AspNetUsers и выберите Показать данные таблицы.
    Снимок экрана, на котором показана схема a S P Net Users. Выделен столбец Email Подтверждено с меткой False.
    На данный момент сообщение электронной почты не подтверждено.

  7. Щелкните строку и выберите удалить. Вы добавите это сообщение еще раз на следующем шаге и отправите сообщение электронной почты с подтверждением.

Подтверждение Email

Рекомендуется подтвердить адрес электронной почты новой регистрации пользователя, чтобы убедиться, что он не олицетворяет кого-то другого (т. е. он не зарегистрирован с чужой электронной почтой). Предположим, у вас есть форум для обсуждения, и вы хотите запретить "bob@example.com" регистрацию в качестве "joe@contoso.com". Без подтверждения "joe@contoso.com" сообщения электронной почты может получать нежелательные сообщения электронной почты из вашего приложения. Предположим, Что Боб случайно зарегистрировался как и не заметил этого, он не сможет использовать восстановление пароля, так как "bib@example.com" приложение не имеет правильного адреса электронной почты. Email подтверждение обеспечивает только ограниченную защиту от ботов и не обеспечивает защиту от определенных спамеров, у них есть много рабочих псевдонимов электронной почты, которые они могут использовать для регистрации.

Как правило, вы хотите запретить новым пользователям публиковать какие-либо данные на веб-сайте, прежде чем они будут подтверждены электронной почтой, SMS-сообщением или другим механизмом. В разделах ниже мы включим подтверждение по электронной почте и изменим код, чтобы предотвратить вход новых зарегистрированных пользователей до тех пор, пока их электронная почта не будет подтверждена.

Подключение SendGrid

Инструкции в этом разделе не являются актуальными. Обновленные инструкции см. в разделе Настройка поставщика электронной почты SendGrid .

Хотя в этом руководстве показано, как добавлять уведомления по электронной почте только с помощью SendGrid, вы можете отправлять сообщения электронной почты с помощью SMTP и других механизмов (см. дополнительные ресурсы).

  1. В консоли диспетчера пакетов введите следующую команду.

    Install-Package SendGrid
    
  2. Перейдите на страницу регистрации Azure SendGrid и зарегистрируйтесь для получения бесплатной учетной записи SendGrid. Настройте SendGrid, добавив код, аналогичный следующему в App_Start/IdentityConfig.cs:

    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>

Запустите приложение и щелкните ссылку Зарегистрировать. После отправки формы регистрации вы войдете в систему.

Снимок экрана, на котором показана домашняя страница my AS P.

Проверьте учетную запись электронной почты и щелкните ссылку, чтобы подтвердить адрес электронной почты.

Требовать подтверждение по электронной почте перед вхоской

В настоящее время после заполнения формы регистрации пользователь входит в систему. Как правило, вы хотите подтвердить его адрес электронной почты, прежде чем войти в систему. В приведенном ниже разделе мы изменим код, чтобы новые пользователи имели подтвержденное сообщение электронной почты, прежде чем они будут входить в систему (проходить проверку подлинности). Обновите 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. Вы можете щелкнуть ссылку Контакт , чтобы убедиться, что анонимные пользователи не имеют доступа и пользователи, прошедшие проверку подлинности, имеют доступ.

[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);
}

Удалите символы комментариев из ForgotPasswordActionLink в файле представления Razor Views\Account\Login.cshtml :

@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 , связанные с этой учетной записью.

Снимок экрана, на котором показана точка My A S P. Net Управление учетной записью. Рядом со строкой Внешние имена входа выделены 0 и ссылка Управление.

Щелкните ссылку на другую службу входа и примите запросы приложения. Две учетные записи были объединены, и вы сможете войти в систему с помощью любой из них. Вы можете захотеть, чтобы пользователи добавляли локальные учетные записи в случае, если служба проверки подлинности в социальных сетях не работает или, скорее всего, они потеряли доступ к своей учетной записи социальной сети.

На следующем изображении Tom — это вход в социальную сеть (который можно увидеть на странице Внешние имена входа: 1 ).

Снимок экрана, на котором показана точка My A S P. Net Управление учетной записью. Выделены строки Выбор пароля и Внешние имена входа.

Щелкнув Выбрать пароль , вы можете добавить локальный вход, связанный с той же учетной записью.

Снимок экрана: страница My A S P dot Net Create Local Login (Создание локального входа). Пример пароля вводится в текстовых полях New password (Новый пароль) и Confirm new password (Подтверждение нового пароля).

Email более подробное подтверждение

Дополнительные сведения см. в руководстве по подтверждению учетной записи и восстановлению пароля с помощью ASP.NET identity .

Отладка приложения

Если вы не получили сообщение электронной почты со ссылкой:

  • Проверьте папку нежелательной почты или спама.
  • Войдите в учетную запись SendGrid и щелкните ссылку Email Действие.

Чтобы проверить ссылку для проверки без электронной почты, скачайте готовый пример. Ссылка для подтверждения и коды подтверждения будут отображаться на странице.

Дополнительные ресурсы