快速掌握 ASP.NET 身份认证框架 Identity - 通过邮件重置密码

这是 ASP.NET Core Identity 系列的第四篇文章,上一篇文章讲解了如何在 ASP.NET Core Identity 中实现用户登录与登出。

这篇文章讲一讲如何在 ASP.NET Core Identity 中通过邮件服务实现用户账号的密码重置。

点击上方或后方蓝字,阅读 ASP.NET Core Identity 系列合集。.

本篇文章的示例项目:https://github.com/zilor-net/IdentitySample/tree/main/Sample04

密码重置

用户管理中最常见的功能就是密码重置。

密码重置过程,不应该涉及系统管理员,因为用户本身应该能够独立完成整个过程。

通常,登录页面上都会为用户提供忘记密码的链接,以用来重置密码,这就是我们接下来要实现的功能。

简单地解释一下密码重置过程:

  1. 用户单击忘记密码链接,然后跳转到带有电子邮件字段的页面。

  2. 用户填写该字段后,应用程序会向该电子邮件发送密码重置的连接。

  3. 用户通过单击电子邮件的密码重置链接,此时会使用密码重置令牌,重定向到密码重置页面。

  4. 用户填充表单中的所有字段后,应用程序将重置密码,用户再被重定向到登录页面或主页。

邮件服务

示例项目中已经集成了邮件服务 「EmailService」 ,以用来帮助我们发送邮件,

具体的邮件发送的实现不是这个系列的主题,就不做过多的阐述。大家可以自己查看示例中「EmailService」项目中关于邮件发送的代码。

邮件服务通过扩展方法注册到了依赖注入框架中,其具体配置在 「appsettings.json」 中。

忘记密码

首先,我们需要创建 「忘记密码」 的视图。

在 「Models」 文件夹中,创建一个 「ForgotPasswordModel」 类:

public class ForgotPasswordModel
{
    [Display(Name = "电子邮箱")]
    [Required(ErrorMessage = "电子邮箱不能为空")]
    [EmailAddress(ErrorMessage = "电子邮箱格式不正确")]
    public string Email { get; set; }
}

它会用在「忘记密码」视图中,这里我们只需要获取用户的电子邮件,所以这里只有一个 「Email」 属性。

接下来,在 「Account」 控制器中,创建两个操作方法:

[HttpGet]
public IActionResult ForgotPassword()
{
    return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordModel forgotPasswordModel)
{
    return View(forgotPasswordModel);
}

public IActionResult ForgotPasswordConfirmation()
{
    return View();
}

这个套路我们已经很熟悉了,第一个 「ForgotPassword」 只是为了创建视图;第二个 「ForgotPassword」 是为了实现逻辑;「ForgotPasswordConfirmation」 则是返回确认视图。

接下来,再依次创建相关的视图:

快速掌握 ASP.NET 身份认证框架 Identity - 通过邮件重置密码
<h1>ForgotPasswordConfirmation</h1>

<p>
    重置密码的链接已经发送到您的电子邮箱!
</p>

然后在 「Login」 视图中,添加忘记密码的链接:

<div class="form-group">
    <a asp-action="ForgotPassword">忘记密码</a>
</div>

现在,让我们来实现忘记密码的逻辑:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword([FromServices]IEmailSender emailSender, ForgotPasswordModel forgotPasswordModel)
{
    if (!ModelState.IsValid)
        return View(forgotPasswordModel);

    var user = await _userManager.FindByEmailAsync(forgotPasswordModel.Email);
    if (user == null)
        return RedirectToAction(nameof(ForgotPasswordConfirmation));

    var token = await _userManager.GeneratePasswordResetTokenAsync(user);
    var callback = Url.Action(nameof(ResetPassword), "Account", new { token, email = user.Email }, Request.Scheme);

    var message = new Message(new string[] { user.Email }, "重置密码", callback, null);
    await emailSender.SendEmailAsync(message);

    return RedirectToAction(nameof(ForgotPasswordConfirmation));
}

如果模型有效,就通过用户的电子邮件,从数据库中获取用户。

如果不存在,只需将该用户,重定向到邮件已发送的确认页面,而不是创建用户不存在的消息。

这么做主要是出于安全考虑,以防止有人利用这个功能,验证用户名的有效性。

如果用户存在,就通过 「GeneratePasswordResetTokenAsync」 方法,生成一个令牌,并创建一个回调链接,到我们将用于重置密码逻辑的操作。

最后,我们向用户提供的电子邮件,发送邮件消息,并将用户重定向到确认页面。

现在,程序还无法创建令牌,因为我们还没有注册令牌服务,这需要在注册 「Identity」 方法时进行注册:

builder.Services.AddIdentity<User, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationContext>()
    .AddDefaultTokenProviders();

如果我们希望密码重置令牌只在有限的时间内有效,例如: 2小时,那我们还需要配置令牌生存期:

builder.Services.Configure<DataProtectionTokenProviderOptions>(opt =>
                opt.TokenLifespan = TimeSpan.FromHours(2));

重置密码

接着,我们来实现 「ResetPassword」 重置密码的操作方法,创建一个 「ResetPasswordModel」 类:

public class ResetPasswordModel
{
    [Display(Name = "密码")]
    [Required(ErrorMessage = "密码不能为空")]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Display(Name = "确认密码")]
    [DataType(DataType.Password)]
    [Compare("Password", ErrorMessage = "密码与确认密码不匹配。")]
    public string ConfirmPassword { get; set; }

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

然后,在 「Account」 控制器中,创建 「ResetPassword」 操作方法:

[HttpGet]
public IActionResult ResetPassword(string token, string email)
{
    var model = new ResetPasswordModel { Token = token, Email = email };
    return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordModel resetPasswordModel)
{
    return View();
}

[HttpGet]
public IActionResult ResetPasswordConfirmation()
{
    return View();
}

这里与 「ForgotPassword」 操作类似。

「HttpGet」ResetPassword操作会接受来自电子邮件中,密码重置连接的请求,提取令牌和电子邮件,并创建一个视图。

「HttpPost」ResetPassword操作是处理重置密码的逻辑。

ResetPasswordConfirmation只是一个密码重置的确认视图。

依次创建这些视图:

快速掌握 ASP.NET 身份认证框架 Identity - 通过邮件重置密码

需要注意的是,我们需要把 「Email」 和 「Token」 两个字段隐藏起来,因为这两个值由应用提供,不需要用户设置:

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

「ResetPasswordConfirmation」 视图:

<h1>ResetPasswordConfirmation</h1>
<p>
    您的密码已经重置. 请点击这里 <a asp-action="Login"> 登录 </a>!
</p>

最后,再来修改 「POST」ResetPassword 操作方法:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordModel resetPasswordModel)
{
    if (!ModelState.IsValid)
        return View(resetPasswordModel);

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

    if (user == null)
        RedirectToAction(nameof(ResetPasswordConfirmation));

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

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

    return RedirectToAction(nameof(ResetPasswordConfirmation));
}

首先,检查模型的有效性,以及用户是否存在于数据库中。

之后,使用 「ResetPasswordAsync」 方法,执行密码重置操作。

如果操作失败,就向模型状态添加错误并返回视图。否则,我们将用户重定向到确认页面。

需要注意的是,如果想要测试最终效果,邮件服务的配置以及用户的邮件地址都必须是真实有效的。

小结

现在,我们已经实现了用户通过电子邮件,重置密码的功能,下篇文章将会讲解如何在用户注册时,必须确认电子邮件是否有效的功能。