Аутентификация и авторизация в ASP.NET Core MVC. Сброс пароля

Сброс пароля в ASP.NET Core MVC — один из важнейших механизмов в системе аутентификации пользователей, который необходимо предусмотреть в своем приложении. Если не предусмотреть такой функции в своем приложении, то, в лучшем случае, мы столкнемся с тем, что забывчивые пользователи засыпят нас просьбами поменять/сбросить пароль, в худшем — с оттоком пользователей из приложения. В ASP.NET Core Identity предусмотрен механизм сброса паролей пользователей и в нашу задачу входит только его внедрение в наше приложение.

Механизм сброса пароля в ASP.NET Core MVC

Для начала, рассмотрим в общих чертах механизм сброса пароля в ASP.NET Core MVC. Для того, чтобы сбросить пароль пользователя мы должны:

  1. Сформировать токен (по аналогии с тем, который мы формировали при подтверждении адреса электронной почты пользователя)
  2. Сформировать и отправить ссылку с токеном на электронную почту пользователя
  3. Пользователь должен перейти по ссылке из письма и задать новый пароль для входа в приложении
  4. Пароль должен быть обновлен в базе данных.

Таким образом, в нашем приложении мы должны разработать следующие представления:

  1. Представление для запроса у пользователя его email и представление, подтверждающее успешную отправку ссылки на сброс пароля
  2. Представление для ввода нового пароля пользователем и представление, подтверждающее установку нового пароля

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

Запуск механизма сброса пароля

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

Добавим в папку Areas/Account/Views/Account представление с именем ForgotPassword.cshtml:

ForgotPasswordВ этом представлении мы будем запрашивать у пользователя email для отправки ссылки. Код представления будет следующим:

@model string

@{
    ViewData["Title"] = "Сброс пароля";
}

<h1 class="bg-info text-white">Сброс пароля</h1>

<form asp-action="ForgotPassword" method="post">
    <div class="form-group">
        <label>Email</label>
        <input name="email" class="form-control" />
    </div>
    <button class="btn btn-primary" type="submit">Отправить</button>
</form>

В качестве модели мы используем обычную строку (string) так как от пользователя нам требуется получить только адрес электронной почты. Для отправки данных мы используем метод POST, а в качестве действия контроллера выступает действие ForgotPassword:

<form asp-action="ForgotPassword" method="post">

Добавим в контроллер AccountController необходимые действия с именем ForgotPassword:

первое действие контроллера используется для загрузки представления:

[Route("{area}/{action}")]
[AllowAnonymous]
[HttpGet]
public IActionResult ForgotPassword()
{
    return View();
}

второе действие осуществляет отправку письма пользователю со ссылкой на сброс пароля:

[Route("{area}/{action}")]
[HttpPost]
public async Task<IActionResult> ForgotPassword([Required] string email)
{
    if (!ModelState.IsValid)
        return View(email);

    var user = await _userManager.FindByEmailAsync(email);
    if (user == null)
        return View();
    var token = await _userManager.GeneratePasswordResetTokenAsync(user);
    
    var url = Url.Action("ResetPassword", "Account", new { user.Email, token }, Request.Scheme);
    bool result = _emailHelper.SendEmailResetPassword(user.Email, url);
    if (result == false)
    {
        return BadRequest();
    }
    else
      return RedirectToAction("ForgotPasswordConfirmation");
}

В этом действии мы пробуем найти пользователя по его email и, если такой пользователь найден, то формируется токен на сброс пароля:

var token = await _userManager.GeneratePasswordResetTokenAsync(user);

полученный токен и email пользователя используются для формирования url:

var url = Url.Action("ResetPassword", "Account", new { user.Email, token }, Request.Scheme);

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

bool result = _emailHelper.SendEmailResetPassword(user.Email, url);
if (result == false)
{
    return BadRequest();
}
else
  return RedirectToAction("ForgotPasswordConfirmation");

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

public bool SendEmailResetPassword(string userEmail, string resetLink)
{
    return Send(userEmail, resetLink, "Сброс пароля");
}

Действие ForgotPasswordConfirmation() используется только для отправки пользователю представления:

[Route("{area}/{action}")]
[AllowAnonymous]
public IActionResult ForgotPasswordConfirmation()
{
    return View();
}

Представление с именем ForgotPasswordConfirmation.cshtml также необходимо добавить в папку Areas/Account/Views/Account:

Код представления также максимально простой:

@{
    ViewData["Title"] = "Подтверждение сброса пароля";
}

<h1>Подтверждение сброса пароля</h1>
<p>
    Письмо отправлено. Пожалуйста, проверьте свой почтовый ящик, чтобы сбросить пароль.
</p>

Осталось добавить ссылку на запуск механизма сброса пароля. Для этого, откроем ранее созданное представление Login.cshtml:

и добавим в самом конце кода представления ссылку:

<a href="/account/ForgotPassword">Сбросить пароль</a>

Теперь можно проверить работу приложения. Запустим приложение и перейдем по ссылке входа в приложение:

здесь мы увидим нашу новую ссылку по которой мы должны перейти, чтобы начать сброс пароля:Клик по ссылке откроет наше новое представление:

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

а в приложении мы увидим сообщение:

Теперь необходимо, чтобы пользователь перешел по полученной ссылке и смог ввести новые пароль.

Смена пароля

Процесс сброса пароля будет строится аналогичным образом. То есть, для работы нам потребуется два представления и три действия контроллера. Первое представление будет загружаться при переходе пользователя по ссылке из письма. Назовем это представление ResetPassword.cshtml:

Код представления будет следующим:

@using AspMvcAuth.Models
@model ResetPasswordModel

@{
    ViewData["Title"] = "Сброс пароля";
}

<h1 class="bg-info text-white">Сброс пароля</h1>
<div class="text-danger" asp-validation-summary="All"></div>

<form asp-action="ResetPassword" method="post">
    <div class="form-group">
        <label asp-for="Password"></label>
        <input type="password" asp-for="Password" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="ConfirmPassword"></label>
        <input type="password" asp-for="ConfirmPassword" class="form-control" />
    </div>
    <input type="hidden" asp-for="Email" class="form-control" />
    <input type="hidden" asp-for="Token" class="form-control" />
    <button class="btn btn-primary" type="submit">Отправить</button>
</form>

Так как для смены пароля, помимо самого пароля, мы должны убедиться, что используется действительный токен и email пользователя, то для передачи данных в контроллер предусмотрена отдельная модель ResetPasswordModel. Файл модели можно разместить в любом месте приложения, главное, чтобы мы смогли получить к ней доступ из представления. Код модели будет следующим:

using System.ComponentModel.DataAnnotations;

namespace AspMvcAuth.Models
{
    public class ResetPasswordModel
    {
        [Required]
        public string Password { get; set; }

        [Compare("Password", ErrorMessage = "Пароли не совпадают")]
        public string ConfirmPassword { get; set; }

        public string Email { get; set; }
        public string Token { get; set; }
    }
}

В самой форме мы также будем сохранять полученные из параметров ссылки токен и email:

<input type="hidden" asp-for="Email" class="form-control" />
<input type="hidden" asp-for="Token" class="form-control" />

Отправка данных формы осуществляется с использованием POST-запроса

<form asp-action="ResetPassword" method="post">

Теперь перейдем к действиям контроллера. Действие ResetPassword() для загрузки данных представления:

[Route("{area}/{action}")]
[AllowAnonymous]
public IActionResult ResetPassword(string token, string email)
{
    var model = new ResetPasswordModel { Token = token, Email = email };
    return View(model);
}

здесь мы получаем из параметров запроса токен и адрес электронной почты, создаем объект модели:

var model = new ResetPasswordModel { Token = token, Email = email };

передаем полученный объект в метод View() и загружаем представление. После того, как пользователь введет пароль, мы должны отправить POST-запрос на сервер и обновить пароль. Для этого используется второй вариант действия ResetPassword():

[Route("{area}/{action}")]
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ResetPassword(ResetPasswordModel resetPassword)
{
    if (!ModelState.IsValid)
        return View(resetPassword);

    var user = await _userManager.FindByEmailAsync(resetPassword.Email);
    if (user == null)
        return View(resetPassword);

    var resetPassResult = await _userManager.ResetPasswordAsync(user, resetPassword.Token, resetPassword.Password);
    if (!resetPassResult.Succeeded)
    {
        foreach (var error in resetPassResult.Errors)
            ModelState.AddModelError(error.Code, error.Description);
        return View();
    }
    return RedirectToAction("ResetPasswordConfirmation");
}

Если пользователь допустит ошибку (введенные пароли не будут совпадать), то снова будет загружено представление в котором пользователь увидит текст ошибки:

if (!ModelState.IsValid) 
    return View(resetPassword);

Далее, мы пытаемся найти пользователя по его email

var user = await _userManager.FindByEmailAsync(resetPassword.Email);

для упрощения, в случае, если пользователь не будет найден по какой-либо причине, то происходит снова загрузка представления ResetPassword.cshtml

if (user == null) 
   return View(resetPassword);

в реальном же приложение, по-хорошему, стоит предусмотреть отдельную обработку такой ситуации, например, перенаправить пользователя на представление с описанием текста ошибки или ещё каким-либо образом оповестить пользователя о том, что пользователь с заданным email не был обнаружен в базе данных.

Далее, мы пытаемся обновить пароль пользователя, используя токен из ссылки:

var resetPassResult = await _userManager.ResetPasswordAsync(user, resetPassword.Token, resetPassword.Password);

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

if (!resetPassResult.Succeeded)
{
    foreach (var error in resetPassResult.Errors)
        ModelState.AddModelError(error.Code, error.Description);
    return View();
}

Если же пароль будет успешно установлен, то пользователь перенаправляется на действие ResetPasswordConfirmation():

return RedirectToAction("ResetPasswordConfirmation");

Это действие загружает одноименное представление:

[Route("{area}/{action}")]
public IActionResult ResetPasswordConfirmation()
{
    return View();
}

Само представление должно находиться также в папке Areas/Account/Views/Account

Код представления будет следующим:

@{
    ViewData["Title"] = "Подтверждение сброса пароля";
}

<h1>Подтверждение сброса пароля</h1>

<p>
    Новый пароль установлен. Пожалуйста, <a href="/account/signin">войдите в приложение</a>
</p>

то есть, всё, что мы делаем с помощью этого представления — это сообщаем пользователю об успешной смене пароля и предлагаем войти в приложение, используя новый пароль.

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

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

Скачать код проекта из Github

Итого

Сброс пароля в ASP.NET Core MVC осуществляется в два этапа. На первом этапе мы по полученному от пользователя адресу электронной почты формируем токен на сброс пароля, формируем ссылку с этим токеном и отправляем её пользователю. На втором этапе пользователь должен перейти по полученной ссылке, ввести в форме  новый пароль и отправить новый пароль вместе с токеном на сервер. На сервере производится сброс пароля и, после этого, пользователь может воспользоваться новым паролем.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии