使用 ASP.NET Identity (C#) 进行帐户确认和密码恢复
在完成本教程之前,应首先完成 使用登录、电子邮件确认和密码重置创建安全 ASP.NET MVC 5 Web 应用。 本教程包含更多详细信息,并演示如何设置本地帐户确认的电子邮件,并允许用户在 ASP.NET 标识中重置忘记的密码。
本地用户帐户要求用户为帐户创建密码,并且该密码存储在 web 应用中 (安全地) 。 ASP.NET Identity 还支持社交帐户,无需用户为应用创建密码。 社交帐户 使用 Google、Twitter、Facebook 或 Microsoft) 等第三方 (对用户进行身份验证。 本主题涵盖以下内容:
- 创建 ASP.NET MVC 应用 并探索 ASP.NET 标识功能。
- 生成标识示例
- 设置电子邮件确认
新用户注册其电子邮件别名,这将创建本地帐户。
选择“注册”按钮会将包含验证令牌的确认电子邮件发送到其电子邮件地址。
向用户发送一封电子邮件,其中包含其帐户的确认令牌。
选择该链接将确认帐户。
密码恢复/重置
忘记密码的本地用户可以将安全令牌发送到其电子邮件帐户,使他们能够重置其密码。
用户将很快收到一封电子邮件,其中包含允许他们重置其密码的链接。
选择该链接会将他们带到“重置”页。
选择“ 重置 ”按钮将确认密码已重置。
创建 ASP.NET Web 应用
首先安装并运行 Visual Studio 2017。
创建新的 ASP.NET Web 项目并选择 MVC 模板。 Web Forms还支持 ASP.NET 标识,因此可以在 Web 窗体应用中执行类似的步骤。
将身份验证更改为 个人用户帐户。
运行应用,选择“ 注册” 链接并注册用户。 此时,对电子邮件的唯一验证是使用 [EmailAddress] 属性。
在服务器资源管理器中,导航到 “数据连接\DefaultConnection\Tables\AspNetUsers”,右键单击并选择“ 打开表定义”。
下图显示了架构
AspNetUsers
:右键单击 “AspNetUsers ”表,然后选择“ 显示表数据”。
此时尚未确认电子邮件。
ASP.NET 标识的默认数据存储是 Entity Framework,但你可以将其配置为使用其他数据存储并添加其他字段。 请参阅本教程末尾的 “其他资源” 部分。
当应用启动并调用 ConfigureAuth
App_Start\Startup.Auth.cs 中的 方法时,将调用 ( Startup.cs ) 的 OWIN 启动类,该方法配置 OWIN 管道并初始化 ASP.NET 标识。 检查 ConfigureAuth
方法。 每个调用都会 CreatePerOwinContext
注册一个回调 (保存在) 中 OwinContext
,该回调将在每个请求中调用一次,以创建指定类型的实例。 可以在每种类型的构造函数和 Create
方法中设置一个断点 (ApplicationDbContext, ApplicationUserManager
) ,并验证是否在每个请求中调用它们。 和 ApplicationUserManager
的ApplicationDbContext
实例存储在 OWIN 上下文中,可以在整个应用程序中访问。 ASP.NET 标识通过 Cookie 中间件挂钩到 OWIN 管道。 有关详细信息,请参阅 ASP.NET Identity 中 UserManager 类的每个请求生存期管理。
更改安全配置文件时,将生成新的安全标记并将其存储在 SecurityStamp
AspNetUsers 表的 字段中。 请注意,字段 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 文件包含 ApplicationUserManager
、 EmailService
和 SmsService
类。 EmailService
和 SmsService
类各自实现 IIdentityMessageService
接口,因此每个类中都有用于配置电子邮件和短信的常用方法。 虽然本教程仅介绍如何通过 SendGrid 添加电子邮件通知,但你可以使用 SMTP 和其他机制发送电子邮件。
该 Startup
类还包含用于 (Facebook、Twitter 等) 添加社交登录的模板,有关详细信息,请参阅我的教程 MVC 5 App with Facebook、Twitter、LinkedIn和 Google OAuth2 登录 。
ApplicationUserManager
检查 类,该类包含用户标识信息并配置以下功能:
- 密码强度要求。
- 用户锁定 (尝试和时间) 。
- 双因素身份验证 (2FA) 。 我将在另一个教程中介绍 2FA 和短信。
- 挂接电子邮件和短信服务。 (,我将在另一个教程) 中介绍短信。
类 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 标识和 OWIN Cookie 身份验证基于声明,因此框架要求应用为用户生成 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));
}
使用 Facebook、Twitter、LinkedIn 和 Google OAuth2 登录的 MVC 5 应用 演示了如何向 ApplicationUser
类添加其他属性。
Email确认
最好确认新用户注册的电子邮件,以验证他们没有模拟其他人 (也就是说,他们尚未注册其他人的电子邮件) 。 假设你有一个讨论论坛,你希望阻止 "bob@example.com"
注册为 "joe@contoso.com"
。 如果没有电子邮件确认, "joe@contoso.com"
可能会从应用收到不需要的电子邮件。 假设 Bob 意外注册为 "bib@example.com"
,并且没有注意到它,他无法使用密码恢复,因为应用没有他正确的电子邮件。 Email确认仅对机器人提供有限的保护,并且不提供针对确定的垃圾邮件发送者的保护,它们有许多可用于注册的工作电子邮件别名。在下面的示例中,用户将无法更改其密码,直到他们选择在其注册的电子邮件帐户上收到的确认链接 (确认帐户。) 可以将此工作流应用于其他方案,例如发送一个链接以确认和重置管理员创建的新帐户的密码, 更改其个人资料后,向用户发送电子邮件等。 你通常希望防止新用户在通过电子邮件、短信或其他机制确认之前将任何数据发布到网站。
生成更完整的示例
在本部分中,你将使用 NuGet 下载我们将要使用的更完整的示例。
创建新的 空 ASP.NET Web 项目。
在包管理器控制台中,输入以下命令:
Install-Package SendGrid Install-Package -Prerelease Microsoft.AspNet.Identity.Samples
在本教程中,我们将使用 SendGrid 发送电子邮件。 包
Identity.Samples
将安装我们将使用的代码。将 项目设置为使用 SSL。
通过运行应用、选择 “注册” 链接并发布注册表单来测试本地帐户的创建。
选择模拟电子邮件确认的演示电子邮件链接。
从示例中删除演示电子邮件链接确认代码 (
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.Text
和 myMessage.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 使用帐户锁定。
其他资源
- ASP.NET Identity 的自定义存储提供程序概述
- 使用 Facebook、Twitter、LinkedIn和 Google OAuth2 登录的 MVC 5 应用 还演示了如何将个人资料信息添加到用户表。
- ASP.NET MVC 和标识 2.0:了解 John Atten 的基础知识。
- ASP.NET Identity 简介
- Pranav Rastogi 宣布推出 ASP.NET Identity 2.0.0 的 RTM。