使用 ASP.NET Identity (C#) 进行帐户确认和密码恢复

在完成本教程之前,应首先完成 使用登录、电子邮件确认和密码重置创建安全 ASP.NET MVC 5 Web 应用。 本教程包含更多详细信息,并演示如何设置本地帐户确认的电子邮件,并允许用户在 ASP.NET 标识中重置忘记的密码。

本地用户帐户要求用户为帐户创建密码,并且该密码存储在 web 应用中 (安全地) 。 ASP.NET Identity 还支持社交帐户,无需用户为应用创建密码。 社交帐户 使用 Google、Twitter、Facebook 或 Microsoft) 等第三方 (对用户进行身份验证。 本主题涵盖以下内容:

新用户注册其电子邮件别名,这将创建本地帐户。

帐户注册窗口的图像

选择“注册”按钮会将包含验证令牌的确认电子邮件发送到其电子邮件地址。

显示电子邮件发送确认的图像

向用户发送一封电子邮件,其中包含其帐户的确认令牌。

确认令牌的图像

选择该链接将确认帐户。

确认电子邮件地址的图像

密码恢复/重置

忘记密码的本地用户可以将安全令牌发送到其电子邮件帐户,使他们能够重置其密码。

忘记密码重置窗口的图像

用户将很快收到一封电子邮件,其中包含允许他们重置其密码的链接。

显示重置密码电子邮件的图像
选择该链接会将他们带到“重置”页。

显示用户密码重置窗口的图像

选择“ 重置 ”按钮将确认密码已重置。

显示密码重置确认的图像

创建 ASP.NET Web 应用

首先安装并运行 Visual Studio 2017

  1. 创建新的 ASP.NET Web 项目并选择 MVC 模板。 Web Forms还支持 ASP.NET 标识,因此可以在 Web 窗体应用中执行类似的步骤。

  2. 将身份验证更改为 个人用户帐户

  3. 运行应用,选择“ 注册” 链接并注册用户。 此时,对电子邮件的唯一验证是使用 [EmailAddress] 属性。

  4. 在服务器资源管理器中,导航到 “数据连接\DefaultConnection\Tables\AspNetUsers”,右键单击并选择“ 打开表定义”。

    下图显示了架构 AspNetUsers

    显示 A p Net Users 架构的图像

  5. 右键单击 “AspNetUsers ”表,然后选择“ 显示表数据”。

    显示表数据的图像

    此时尚未确认电子邮件。

ASP.NET 标识的默认数据存储是 Entity Framework,但你可以将其配置为使用其他数据存储并添加其他字段。 请参阅本教程末尾的 “其他资源” 部分。

当应用启动并调用 ConfigureAuthApp_Start\Startup.Auth.cs 中的 方法时,将调用 ( Startup.cs ) 的 OWIN 启动类,该方法配置 OWIN 管道并初始化 ASP.NET 标识。 检查 ConfigureAuth 方法。 每个调用都会 CreatePerOwinContext 注册一个回调 (保存在) 中 OwinContext ,该回调将在每个请求中调用一次,以创建指定类型的实例。 可以在每种类型的构造函数和 Create 方法中设置一个断点 (ApplicationDbContext, ApplicationUserManager) ,并验证是否在每个请求中调用它们。 和 ApplicationUserManagerApplicationDbContext实例存储在 OWIN 上下文中,可以在整个应用程序中访问。 ASP.NET 标识通过 Cookie 中间件挂钩到 OWIN 管道。 有关详细信息,请参阅 ASP.NET Identity 中 UserManager 类的每个请求生存期管理

更改安全配置文件时,将生成新的安全标记并将其存储在 SecurityStampAspNetUsers 表的 字段中。 请注意,字段 SecurityStamp 不同于安全 Cookie。 安全 Cookie 不会存储在 AspNetUsers 表 (或标识 DB) 的任何其他位置。 安全 Cookie 令牌是使用 DPAPI 自签名的,并使用 UserId, SecurityStamp 和 过期时间信息创建。

Cookie 中间件会检查每个请求上的 Cookie。 SecurityStampValidator类中的 Startup 方法命中 DB 并定期检查安全标记,如 指定的validateInterval一样。 除非更改安全配置文件,否则仅在示例) (每隔 30 分钟发生一次。 选择 30 分钟的间隔以最大程度地减少数据库行程。 有关更多详细信息,请参阅我的 双因素身份验证教程

根据代码中的注释, UseCookieAuthentication 方法支持 Cookie 身份验证。 字段 SecurityStamp 和关联的代码为应用提供了额外的安全层,当你更改密码时,你将从登录的浏览器注销。 方法 SecurityStampValidator.OnValidateIdentity 使应用能够在用户登录时验证安全令牌,在更改密码或使用外部登录名时使用安全令牌。 这是为了确保使用旧密码生成的任何令牌 (cookie) 无效。 在示例项目中,如果更改用户密码,则会为用户生成新令牌,以前的任何令牌都会失效, SecurityStamp 字段将更新。

标识系统允许配置应用,以便在用户安全配置文件更改 (例如,当用户更改其密码或更改关联的登录 ((例如从 Facebook、Google、Microsoft 帐户等) )时,用户将从所有浏览器实例中注销。 例如,下图显示了 单一注销示例 应用,该应用允许用户通过选择一个按钮 (注销所有浏览器实例, IE、Firefox 和 Chrome) 。 或者,此示例只允许注销特定的浏览器实例。

显示单一注销示例应用窗口的图像

单一注销示例应用演示了 ASP.NET Identity 如何允许重新生成安全令牌。 这是为了确保使用旧密码生成的任何令牌 (cookie) 无效。 此功能为应用程序提供额外的安全层;更改密码时,将在登录此应用程序的位置注销。

App_Start\IdentityConfig.cs 文件包含 ApplicationUserManagerEmailServiceSmsService 类。 EmailServiceSmsService 类各自实现 IIdentityMessageService 接口,因此每个类中都有用于配置电子邮件和短信的常用方法。 虽然本教程仅介绍如何通过 SendGrid 添加电子邮件通知,但你可以使用 SMTP 和其他机制发送电子邮件。

Startup 类还包含用于 (Facebook、Twitter 等) 添加社交登录的模板,有关详细信息,请参阅我的教程 MVC 5 App with Facebook、Twitter、LinkedIn和 Google OAuth2 登录

ApplicationUserManager检查 类,该类包含用户标识信息并配置以下功能:

  • 密码强度要求。
  • 用户锁定 (尝试和时间) 。
  • 双因素身份验证 (2FA) 。 我将在另一个教程中介绍 2FA 和短信。
  • 挂接电子邮件和短信服务。 (,我将在另一个教程) 中介绍短信。

ApplicationUserManager 派生自泛型 UserManager<ApplicationUser> 类。 ApplicationUser 派生自 IdentityUserIdentityUser 派生自泛型 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

ApplicationUserpublic 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 标识和 OWIN Cookie 身份验证基于声明,因此框架要求应用为用户生成 ClaimsIdentityClaimsIdentity 包含有关用户的所有声明的信息,例如用户的名称、年龄和用户所属的角色。 在此阶段,还可以为用户添加更多声明。

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

使用 Facebook、Twitter、LinkedIn 和 Google OAuth2 登录的 MVC 5 应用 演示了如何向 ApplicationUser 类添加其他属性。

Email确认

最好确认新用户注册的电子邮件,以验证他们没有模拟其他人 (也就是说,他们尚未注册其他人的电子邮件) 。 假设你有一个讨论论坛,你希望阻止 "bob@example.com" 注册为 "joe@contoso.com"。 如果没有电子邮件确认, "joe@contoso.com" 可能会从应用收到不需要的电子邮件。 假设 Bob 意外注册为 "bib@example.com" ,并且没有注意到它,他无法使用密码恢复,因为应用没有他正确的电子邮件。 Email确认仅对机器人提供有限的保护,并且不提供针对确定的垃圾邮件发送者的保护,它们有许多可用于注册的工作电子邮件别名。在下面的示例中,用户将无法更改其密码,直到他们选择在其注册的电子邮件帐户上收到的确认链接 (确认帐户。) 可以将此工作流应用于其他方案,例如发送一个链接以确认和重置管理员创建的新帐户的密码, 更改其个人资料后,向用户发送电子邮件等。 你通常希望防止新用户在通过电子邮件、短信或其他机制确认之前将任何数据发布到网站。

生成更完整的示例

在本部分中,你将使用 NuGet 下载我们将要使用的更完整的示例。

  1. 创建新的 ASP.NET Web 项目。

  2. 在包管理器控制台中,输入以下命令:

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

    在本教程中,我们将使用 SendGrid 发送电子邮件。 包 Identity.Samples 将安装我们将使用的代码。

  3. 项目设置为使用 SSL

  4. 通过运行应用、选择 “注册” 链接并发布注册表单来测试本地帐户的创建。

  5. 选择模拟电子邮件确认的演示电子邮件链接。

  6. 从示例中删除演示电子邮件链接确认代码 (ViewBag.Link 帐户控制器中的代码。 DisplayEmail 请参阅 和 ForgotPasswordConfirmation 操作方法和剃须刀视图) 。

警告

如果更改此示例中的任何安全设置,生产应用将需要进行安全审核,以显式调用所做的更改。

检查 App_Start\IdentityConfig.cs 中的代码

此示例演示如何创建帐户并将其添加到 管理员 角色。 应将示例中的电子邮件替换为将用于管理员帐户的电子邮件。 现在创建管理员帐户的最简单方法是在 方法中 Seed 以编程方式创建。 我们希望将来有一个工具,允许你创建和管理用户和角色。 示例代码确实允许创建和管理用户和角色,但必须先具有管理员帐户才能运行角色和用户管理员页面。 在此示例中,管理员帐户是在为数据库设定种子时创建的。

更改密码并将名称更改为可以接收电子邮件通知的帐户。

警告

安全性 - 切勿将敏感数据存储在源代码中。

如前所述, app.CreatePerOwinContext 启动类中的调用将回调添加到 Create 应用 DB 内容、用户管理器和角色管理器类的 方法。 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 标识数据存储中。 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);
      }
   }
}

注意

Email客户端通常只接受文本消息, (不接受 HTML) 。 应以文本和 HTML 格式提供消息。 在上面的 SendGrid 示例中,这是使用上面所示的 myMessage.TextmyMessage.Html 代码完成的。

以下代码演示如何使用 MailMessage 类发送电子邮件,其中 message.Body 仅返回链接。

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 电子邮件帐户执行此操作,请参阅 John Atten 的 OUTLOOK.COM SMTP 主机的 C# SMTP 配置 及其ASP.NET Identity 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);
}

如果未确认用户电子邮件,方法将失败。 如果发布无效电子邮件地址的错误,恶意用户可能会使用该信息来查找有效的 userId (电子邮件别名) 攻击。

以下代码显示了 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();
}

使用忘记的密码令牌后,该令牌将失效。 App_Start\IdentityConfig.cs 文件中方法 (的以下代码更改Create) 将令牌设置为在 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:设置帐户验证和由 John Atten Two-Factor授权。 尽管可以在登录密码尝试失败时设置帐户锁定,但这种方法会使登录容易受到 DOS 锁定的影响。 建议仅对 2FA 使用帐户锁定。

其他资源