Поделиться через


Подтверждение учетной записи и восстановление пароля с помощью ASP.NET Identity (C#)

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

Для учетной записи локального пользователя требуется создать пароль для учетной записи, а этот пароль хранится (безопасно) в веб-приложении. ASP.NET Identity также поддерживает учетные записи социальных сетей, которые не требуют от пользователя создания пароля для приложения. учетные записи социальных сетей используют сторонний сервис (например, Google, Twitter, Facebook или Майкрософт) для аутентификации пользователей. В этом разделе рассматривается следующее:

Новые пользователи регистрируют псевдоним электронной почты, который создает локальную учетную запись.

изображение окна регистрации учетной записи

При нажатии кнопки "Регистрация" отправляется сообщение с подтверждением, содержащее маркер проверки, на адрес электронной почты.

изображение с подтверждением отправки электронной почты

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

изображение маркера подтверждения

Выбор ссылки подтверждает учетную запись.

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

Восстановление и сброс пароля

Локальные пользователи, которые забыли пароль, могут отправить маркер безопасности в свою учетную запись электронной почты, что позволит им сбросить пароль.

изображение окна сброса забытого пароля

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

Изображение , показывающее email для сброса пароля
При выборе ссылки они перейдут на страницу "Сброс".

изображение с окном сброса пароля пользователя

Выбор кнопки Сброс подтвердит сброс пароля.

Изображение с подтверждением сброса пароля

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

Начните с установки и запуска Visual Studio 2017.

  1. Создайте новый веб-проект ASP.NET и выберите шаблон MVC. Веб-формы также поддерживают ASP.NET identity, поэтому вы можете выполнить аналогичные действия в приложении веб-форм.

  2. Измените аутентификацию на отдельные учетные записи пользователей.

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

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

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

    изображение , показывающее схему ASP.NET Users

  5. Щелкните правой кнопкой мыши таблицу AspNetUsers и выберите Показать данные таблицы.

    изображение , показывающее данные таблицы

    На этом этапе сообщение электронной почты не было подтверждено.

Хранилище данных по умолчанию для ASP.NET Identity — Entity Framework, но его можно настроить для использования других хранилищ данных и добавления дополнительных полей. См. раздел дополнительные ресурсы в конце этого руководства.

Класс запуска OWIN (Startup.cs) вызывается при запуске приложения и вызывает метод в App_Start\Startup.Auth.cs, который настраивает конвейер OWIN и инициализирует ASP.NET Identity. Проверьте метод ConfigureAuth. Каждый вызов CreatePerOwinContext регистрирует обратный вызов (сохраненный в OwinContext), который будет вызываться один раз на запрос для создания экземпляра указанного типа. Можно задать точку останова в конструкторе и методе Create каждого типа (ApplicationDbContext, ApplicationUserManager) и проверить их вызов по каждому запросу. Экземпляры ApplicationDbContext и ApplicationUserManager хранятся в контексте OWIN, к которому можно получить доступ во всем приложении. ASP.NET Identity интегрируется в конвейер OWIN через посредник cookie. Дополнительные сведения см. в разделе Управление временем существования запросов для класса UserManager в ASP.NET Identity.

При изменении профиля безопасности создается новая метка безопасности и хранится в поле SecurityStamp таблицы AspNetUsers. Обратите внимание, что поле SecurityStamp отличается от файла cookie безопасности. Файл cookie безопасности не хранится в таблице AspNetUsers (или в другом месте в базе данных удостоверений). Маркер cookie безопасности самозаверяется с помощью DPAPI и создается с UserId, SecurityStamp и сведениями о времени окончания срока действия.

Промежуточное программное обеспечение cookie проверяет cookie при каждом запросе. Метод SecurityStampValidator в классе Startup периодически попадает в базу данных и проверяет метку безопасности, как указано в validateInterval. Это происходит только каждые 30 минут (в нашем примере), если вы не измените профиль безопасности. Был выбран 30-минутный интервал, чтобы свести к минимуму поездки в базу данных. Дополнительные сведения см. в руководстве по двухфакторной проверке подлинности .

Согласно комментариям в коде, метод UseCookieAuthentication поддерживает проверку подлинности куки. Поле SecurityStamp и связанный код обеспечивает дополнительный уровень безопасности для приложения при изменении пароля, вы будете выходить из браузера, с которым вы вошли. Метод SecurityStampValidator.OnValidateIdentity позволяет приложению проверять маркер безопасности при входе пользователя, который используется при изменении пароля или использовании внешнего входа. Это необходимо, чтобы все маркеры (файлы cookie), созданные с помощью старого пароля, были недействительными. В примере проекта при изменении пароля пользователей создается новый маркер для пользователя, все предыдущие маркеры недействительны, а поле SecurityStamp обновляется.

Система удостоверения позволяет настроить приложение так, чтобы при изменении профиля безопасности пользователя (например, когда пользователь изменяет пароль или связанное имя входа, такое как Facebook, Google, учетная запись Майкрософт и т. д.) пользователь выходит из всех сеансов браузера. Например, на рисунке ниже показано приложение Single signout sample, которое позволяет пользователю выйти из всех экземпляров браузера (в данном случае IE, Firefox и Chrome), нажав одну кнопку. Кроме того, в примере можно выйти только из определенного экземпляра браузера.

изображение, показывающее пример окна приложения с функцией единого выхода из системы

В примере приложения с единой системой выхода показано, как ASP.NET Identity позволяет повторно сгенерировать токен безопасности. Это необходимо, чтобы все маркеры (файлы cookie), созданные с помощью старого пароля, были недействительными. Эта функция обеспечивает дополнительный уровень безопасности для приложения; При изменении пароля вы будете выходить из системы, где вы вошли в это приложение.

Файл App_Start\IdentityConfig.cs содержит классы ApplicationUserManager, EmailService и SmsService. Классы EmailService и SmsService реализуют интерфейс IIdentityMessageService, поэтому в каждом классе используются общие методы для настройки электронной почты и SMS. Хотя в этом руководстве показано, как добавлять уведомления по электронной почте с помощью SendGrid, вы можете отправлять сообщения электронной почты с помощью SMTP и других механизмов.

Класс Startup также содержит шаблон для добавления социальных входов (Facebook, Twitter и т. д.), подробнее см. в моем руководстве по приложению MVC 5 с Facebook, Twitter, LinkedIn и Google OAuth2 для входа.

Изучите класс ApplicationUserManager, который содержит сведения об удостоверениях пользователей и настраивает следующие функции:

  • Требования к надежности паролей.
  • Блокировка пользователя (попытки и время).
  • Двухфакторная проверка подлинности (2FA). Я расскажу о 2FA и SMS в другом учебнике.
  • Подключение служб электронной почты и SMS. (Я освещу SMS в другом уроке).

Класс ApplicationUserManager является производным от универсального класса UserManager<ApplicationUser>. ApplicationUser является производным от IdentityUser. IdentityUser является производным от универсального класса IdentityUser:

//     Default EntityFramework IUser implementation
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
   where TLogin : IdentityUserLogin<TKey>
   where TRole : IdentityUserRole<TKey>
   where TClaim : IdentityUserClaim<TKey>
{
   public IdentityUser()
   {
      Claims = new List<TClaim>();
      Roles = new List<TRole>();
      Logins = new List<TLogin>();
   }

   ///     User ID (Primary Key)
   public virtual TKey Id { get; set; }

   public virtual string Email { get; set; }
   public virtual bool EmailConfirmed { get; set; }

   public virtual string PasswordHash { get; set; }

   ///     A random value that should change whenever a users credentials have changed (password changed, login removed)
   public virtual string SecurityStamp { get; set; }

   public virtual string PhoneNumber { get; set; }
   public virtual bool PhoneNumberConfirmed { get; set; }

   public virtual bool TwoFactorEnabled { get; set; }

   ///     DateTime in UTC when lockout ends, any time in the past is considered not locked out.
   public virtual DateTime? LockoutEndDateUtc { get; set; }

   public virtual bool LockoutEnabled { get; set; }

   ///     Used to record failures for the purposes of lockout
   public virtual int AccessFailedCount { get; set; }
   
   ///     Navigation property for user roles
   public virtual ICollection<TRole> Roles { get; private set; }

   ///     Navigation property for user claims
   public virtual ICollection<TClaim> Claims { get; private set; }

   ///     Navigation property for user logins
   public virtual ICollection<TLogin> Logins { get; private set; }
   
   public virtual string UserName { get; set; }
}

Приведенные выше свойства совпадают со свойствами в таблице AspNetUsers, показанной выше.

Обобщенные аргументы в IUser позволяют создавать производный класс с использованием различных типов для идентификации первичного ключа. См. пример ChangePK, в котором показано, как изменить первичный ключ с строки на int или GUID.

ApplicationUser

ApplicationUser (public class ApplicationUserManager : UserManager<ApplicationUser>) определяется в Models\IdentityModels.cs как:

public class ApplicationUser : IdentityUser
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
        UserManager<ApplicationUser> manager)
    {
        // Note the authenticationType must match the one defined in 
       //   CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, 
    DefaultAuthenticationTypes.ApplicationCookie);
        // Add custom user claims here
        return userIdentity;
    }
}

Выше выделенный код создает ClaimsIdentity. ASP.NET Identity и OWIN Cookie Authentication основаны на утверждениях, поэтому фреймворк требует, чтобы приложение создавало ClaimsIdentity для пользователя. ClaimsIdentity содержит сведения обо всех утверждениях для пользователя, таких как имя пользователя, возраст и роли, к которой принадлежит пользователь. На этом этапе можно добавить дополнительные утверждения для пользователя.

Метод OWIN AuthenticationManager.SignIn передает ClaimsIdentity и авторизует пользователя:

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties(){
       IsPersistent = isPersistent }, 
       await user.GenerateUserIdentityAsync(UserManager));
}

В приложении MVC 5 с входом через Facebook, Twitter, LinkedIn и Google OAuth2 показано, как можно добавить дополнительные свойства в класс ApplicationUser.

Подтверждение электронной почты

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

Постройте более полный образец

В этом разделе вы будете использовать NuGet для скачивания более полного примера, с которым мы будем работать.

  1. Создайте новый пустой веб-проект ASP.NET.

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

    Install-Package SendGrid
    Install-Package -Prerelease Microsoft.AspNet.Identity.Samples
    

    В этом руководстве мы будем использовать SendGrid для отправки электронной почты. Пакет Identity.Samples устанавливает код, с которым мы будем работать.

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

  4. Проверьте создание локальной учетной записи, запустив приложение, выбрав ссылку регистрации и разместив форму регистрации.

  5. Выберите демонстрационную ссылку электронной почты, которая имитирует подтверждение электронной почты.

  6. Удалите код подтверждения ссылки демонстрационной электронной почты из примера (код ViewBag.Link в контроллере учетной записи. См. методы действий DisplayEmail и ForgotPasswordConfirmation и Razor-представления).

Предупреждение

При изменении любой из настроек безопасности в этом примере производственные приложения должны пройти аудит безопасности, в котором явно указываются внесенные изменения.

Проверка кода в App_Start\IdentityConfig.cs

В примере показано, как создать учетную запись и добавить ее в роль администратора. Вы должны заменить электронную почту в примере на адрес электронной почты, который вы будете использовать для учетной записи администратора. Самый простой способ в настоящее время создать учетную запись администратора — программно, в методе Seed. Мы надеемся, что в будущем вы сможете создавать и администрировать пользователей и роли. Пример кода позволяет создавать пользователей и роли, а также управлять ими, но сначала необходимо иметь учетную запись администратора для запуска ролей и страниц администрирования пользователей. В этом примере учетная запись администратора создается при начальном запуске базы данных.

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

Предупреждение

Безопасность— никогда не храните конфиденциальные данные в исходном коде.

Как упоминалось ранее, вызов app.CreatePerOwinContext в классе запуска добавляет обратные вызовы в метод Create классов содержимого базы данных приложения, диспетчера пользователей и менеджера ролей. Конвейер OWIN вызывает метод Create для этих классов для каждого запроса и сохраняет контекст для каждого класса. Контроллер учетной записи предоставляет доступ к менеджеру пользователей из контекста HTTP (который содержит контекст OWIN).

public ApplicationUserManager UserManager
{
    get
    {
        return _userManager ?? 
    HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    }
    private set
    {
        _userManager = value;
    }
}

Когда пользователь регистрирует локальную учетную запись, вызывается метод HTTP Post 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)
        {
            var 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 this link: <a href=\"" 
                                               + callbackUrl + "\">link</a>");
            // ViewBag.Link = callbackUrl;   // Used only for initial demo.
            return View("DisplayEmail");
        }
        AddErrors(result);
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Приведенный выше код использует данные модели для создания новой учетной записи пользователя с помощью введенного сообщения электронной почты и пароля. Если псевдоним электронной почты находится в хранилище данных, создание учетной записи завершается ошибкой, а форма отображается снова. Метод GenerateEmailConfirmationTokenAsync создает безопасный маркер подтверждения и сохраняет его в хранилище данных ASP.NET Identity. Метод Url.Action создает ссылку, содержащую UserId и маркер подтверждения. Затем эта ссылка отправляется пользователю по электронной почте, пользователь может выбрать ссылку в своем почтовом приложении, чтобы подтвердить свою учетную запись.

Настройка подтверждения электронной почты

Перейдите на страницу регистрации SendGrid и зарегистрируйте бесплатную учетную запись. Добавьте код, аналогичный следующему, чтобы настроить SendGrid:

public class EmailService : IIdentityMessageService
{
   public Task SendAsync(IdentityMessage message)
   {
      return configSendGridasync(message);
   }

   private 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)
      {
         return transportWeb.DeliverAsync(myMessage);
      }
      else
      {
         return Task.FromResult(0);
      }
   }
}

Заметка

Клиенты электронной почты часто принимают только текстовые сообщения (без HTML). Необходимо указать сообщение в тексте и HTML. В приведенном выше примере SendGrid это делается с помощью myMessage.Text и myMessage.Html кода, показанного выше.

В следующем коде показано, как отправлять электронную почту с помощью класса MailMessage , где возвращает только ссылку.

void sendMail(Message message)
{
#region formatter
   string text = string.Format("Please click on this link to {0}: {1}", message.Subject, message.Body);
   string html = "Please confirm your account by clicking this link: <a href=\"" + message.Body + "\">link</a><br/>";

   html += HttpUtility.HtmlEncode(@"Or click on the copy the following link on the browser:" + message.Body);
#endregion

   MailMessage msg = new MailMessage();
   msg.From = new MailAddress("joe@contoso.com");
   msg.To.Add(new MailAddress(message.Destination));
   msg.Subject = message.Subject;
   msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
   msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(html, null, MediaTypeNames.Text.Html));

   SmtpClient smtpClient = new SmtpClient("smtp.gmail.com", Convert.ToInt32(587));
   System.Net.NetworkCredential credentials = new System.Net.NetworkCredential("joe@contoso.com", "XXXXXX");
   smtpClient.Credentials = credentials;
   smtpClient.EnableSsl = true;
   smtpClient.Send(msg);
}

Предупреждение

Безопасность— никогда не храните конфиденциальные данные в исходном коде. Учетная запись и учетные данные хранятся в appSetting. В Azure вы можете безопасно хранить эти значения на вкладке Настройка на портале Azure. См. лучшие практики по развертыванию паролей и других конфиденциальных данных в ASP.NET и Azure.

Введите учетные данные SendGrid, запустите приложение, зарегистрируйтесь с помощью псевдонима электронной почты, чтобы выбрать ссылку подтверждения в сообщении электронной почты. Чтобы узнать, как это сделать с Outlook.com учетной записью электронной почты, см. статью конфигурации SMTP C# для Outlook.Com узла SMTP и егоASP.NET удостоверения 2.0. Настройка проверки учетной записи и Two-Factor записи авторизации.

После нажатия кнопки Зарегистрировать на адрес электронной почты отправляется сообщение подтверждения, содержащее маркер проверки.

изображение окна подтверждения отправки электронной почты

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

изображение полученного сообщения электронной почты

Проверка кода

В следующем коде показан метод POST ForgotPassword.

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

        var 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 here: <a href=\"" + callbackUrl + "\">link</a>");        
        return View("ForgotPasswordConfirmation");
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Метод завершается автоматически, если сообщение электронной почты пользователя не было подтверждено. Если ошибка была опубликована для недопустимого адреса электронной почты, злоумышленники могут использовать эти сведения, чтобы найти допустимый идентификатор пользователя (псевдонимы электронной почты) для атаки.

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

public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
    if (userId == null || code == null)
    {
        return View("Error");
    }
    var result = await UserManager.ConfirmEmailAsync(userId, code);
    if (result.Succeeded)
    {
        return View("ConfirmEmail");
    }
    AddErrors(result);
    return View();
}

После использования забытого маркера пароля он является недействительным. Следующее изменение кода в методе Create (в файле App_Start\IdentityConfig.cs) задает срок действия маркеров в течение 3 часов.

if (dataProtectionProvider != null)
 {
    manager.UserTokenProvider =
       new DataProtectorTokenProvider<ApplicationUser>
          (dataProtectionProvider.Create("ASP.NET Identity"))
          {                    
             TokenLifespan = TimeSpan.FromHours(3)
          };
 }

В приведенном выше коде токены для восстановления забытого пароля и подтверждения электронной почты истекают через 3 часа. Значение по умолчанию TokenLifespan — один день.

В следующем коде показан метод подтверждения электронной почты:

// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
   if (userId == null || code == null)
   {
      return View("Error");
   }
   IdentityResult result;
   try
   {
      result = await UserManager.ConfirmEmailAsync(userId, code);
   }
   catch (InvalidOperationException ioe)
   {
      // ConfirmEmailAsync throws when the userId is not found.
      ViewBag.errorMessage = ioe.Message;
      return View("Error");
   }

   if (result.Succeeded)
   {
      return View();
   }

   // If we got this far, something failed.
   AddErrors(result);
   ViewBag.errorMessage = "ConfirmEmail failed";
   return View("Error");
}

Чтобы сделать приложение более безопасным, ASP.NET Identity поддерживает проверку подлинности Two-Factor (2FA). См. ASP.NET Identity 2.0: Настройка проверки учетной записи и Two-Factor авторизации Джоном Аттеном. Несмотря на то что вы можете настроить блокировку учетной записи при неудачных попытках входа в систему, этот подход делает вход уязвимым к блокировкам DOS. Рекомендуется использовать блокировку учетных записей только с 2FA.

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