Сброс пароля в ASP.NET Core MVC — один из важнейших механизмов в системе аутентификации пользователей, который необходимо предусмотреть в своем приложении. Если не предусмотреть такой функции в своем приложении, то, в лучшем случае, мы столкнемся с тем, что забывчивые пользователи засыпят нас просьбами поменять/сбросить пароль, в худшем — с оттоком пользователей из приложения. В ASP.NET Core Identity предусмотрен механизм сброса паролей пользователей и в нашу задачу входит только его внедрение в наше приложение.
Механизм сброса пароля в ASP.NET Core MVC
Для начала, рассмотрим в общих чертах механизм сброса пароля в ASP.NET Core MVC. Для того, чтобы сбросить пароль пользователя мы должны:
- Сформировать токен (по аналогии с тем, который мы формировали при подтверждении адреса электронной почты пользователя)
- Сформировать и отправить ссылку с токеном на электронную почту пользователя
- Пользователь должен перейти по ссылке из письма и задать новый пароль для входа в приложении
- Пароль должен быть обновлен в базе данных.
Таким образом, в нашем приложении мы должны разработать следующие представления:
- Представление для запроса у пользователя его email и представление, подтверждающее успешную отправку ссылки на сброс пароля
- Представление для ввода нового пароля пользователем и представление, подтверждающее установку нового пароля
Также, нам необходимо будет добавить ссылку в представление входа пользователя в приложение для того, чтобы можно было начать процедуру сброса пароля. Соответственно, для этих представлений мы должны будем создать необходимые действия контроллера и модели для работы с данными. Также стоит отметить, что для отправки писем пользователю мы будем также использовать наш сервис который был разработан в этой части.
Запуск механизма сброса пароля
На этом этапе мы должны запросить у пользователя email на который будет отправлена ссылка для сброса пароля, а также подтвердить успешную отправку письма.
Добавим в папку Areas/Account/Views/Account
представление с именем ForgotPassword.cshtml
:
В этом представлении мы будем запрашивать у пользователя 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 осуществляется в два этапа. На первом этапе мы по полученному от пользователя адресу электронной почты формируем токен на сброс пароля, формируем ссылку с этим токеном и отправляем её пользователю. На втором этапе пользователь должен перейти по полученной ссылке, ввести в форме новый пароль и отправить новый пароль вместе с токеном на сервер. На сервере производится сброс пароля и, после этого, пользователь может воспользоваться новым паролем.