领先技术

在 ASP.NET 标识中存储用户数据

Dino Esposito

Dino EspositoVisual Studio 2013 中的 ASP.NET 标识是一种用于简化管理用户数据和建立更高效成员身份系统等枯燥但必需的任务的方式。我之前已概述了 ASP.NET 标识 API (msdn.microsoft.com/magazine/dn605872),并探讨了它参与社交网络和 OAuth 协议的情况 (msdn.microsoft.com/magazine/dn745860)。本文将详细阐述 ASP.NET 标识的扩展点,首先从用户数据表示和基础数据存储开始。

基础工作

首先,在 Visual Studio 2013 中新建一个空 ASP.NET MVC 项目。向导所提供的所有默认设置都是可以的,但请务必选择单用户身份验证模式。基架代码将用户数据存储在本地 SQL Server 文件中,并带有使用以下约定自动生成的名称:aspnet-[ProjectName]-[RandomNumber]。此代码还使用实体框架以读写的方式来访问用户数据库。用户数据表示位于 ApplicationUser 类中:

public class ApplicationUser : IdentityUser
{
}

正如您所见,ApplicationUser 类继承自系统提供的 IdentityUser 类。要自定义用户表示,先在这里向此类中添加一个新成员:

public class ApplicationUser : IdentityUser
{
  public String MagicCode { get; set; }
}

此示例中的类名称 ApplicationUser 不是强制性的,您可以根据需要进行更改。在此示例中,我有意选择了奇怪的“”神奇代码”字段来表示双重可能性。您可以添加需要 UI 的字段,如出生日期、社会保障号或其他任何您要在注册时指定的字段。还可以添加您所需的字段,但这些字段可以在实际创建用户记录时进行静默计算(例如,特定于应用的“神奇”代码)。默认情况下,ApplicationUser 类包含图 1 中所列的成员。

图 1 在 IdentityUser 基类上定义的成员

成员 说明
ID 此表自动生成的唯一标识符 (GUID)。此字段为主键。
UserName 显示用户名称。
PasswordHash 通过提供的密码生成的哈希。
SecurityStamp 在 UserManager 对象生存期的特定时间点自动创建的 GUID。通常是在更改密码、添加或移除社交登录时创建并更新的。如果未做任何更改,安全戳记通常会获取用户信息快照,并让用户自动登录。
Discriminator 此列特定于实体框架持久性模型,并确定特定行所属的类。根目录位于 IdentityUser 中的层次结构的每个类都将拥有唯一的鉴别器值。

 

图 1 中所列的字段以及以编程方式添加到 ApplicationUser 类定义中的任何其他字段,最终都存储在数据库表中。默认表名为 AspNetUser。此外,IdentityUser 类还公开了几个属性,如 Logins、Claims 和 Roles。这些属性不会存储在 AspNetUsers 表中,而是存储在位于同一数据库的其他端表(AspNetUserRoles、AspNetUserLogins 和 AspNetUserClaims)中(请参阅 图 2)。

ASP.NET 标识用户数据库的默认结构
图 2 ASP.NET 标识用户数据库的默认结构

修改 ApplicationUser 类不能确保额外字段立即反映在 UI 中并保存到数据库。幸运的是,更新数据库并不需要太多的工作。

更改基架代码

要反映新字段,您需要编辑应用程序视图和模型。在新用户进行网站注册的应用程序表单中,您需要添加一些标记以便为神奇代码和任何其他附加字段显示 UI。图 3 显示了在注册表单后的 CSHTML Razor 文件的修改后代码。

图 3 向用户展示附加字段的 Razor 文件

@using (Html.BeginForm("Register", "Account",
  FormMethod.Post, new 
    { @class = "form-horizontal", role = "form" }))
{
  @Html.AntiForgeryToken()
  <h4>Create a new account.</h4>
  <hr />
  @Html.ValidationSummary()
  <div class="form-group">
    @Html.LabelFor(m => m.MagicCode, 
      new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
      @Html.TextBoxFor(m => m.MagicCode, 
        new { @class = "form-control" })
    </div>
  </div>   
  ...
}

CSHTML 注册表单基于符合命名约定的 RegisterViewModel 视图模型类。图 4 显示了将 RegisterViewModel 类插入基于数据批注的大多数 ASP.NET MVC 应用程序的传统验证机制所需的更改。

图 4 对注册视图模型类的更改

public class RegisterViewModel
{
  [Required]
  [Display(Name = "User name")]
  public string UserName { get; set; }
  [Required]
  [StringLength(100, ErrorMessage = 
    "The {0} must be at least {1} character long.",
    MinimumLength = 6)]
  [DataType(DataType.Password)]
  [Display(Name = "Password")]
  public string Password { get; set; }
  [DataType(DataType.Password)]
  [Display(Name = "Confirm password")]
  [Compare("Password", ErrorMessage = 
    "Password and confirmation do not match.")]
  public string ConfirmPassword { get; set; }
  [Required]
  [Display(Name = "Internal magic code")]
  public string MagicCode { get; set; }
}

但是这些更改还不够。还需要一个最关键的步骤。您必须将额外数据向下传递至要将其写入永久性存储的层。您需要对在注册表单中处理 POST 操作的控制器方法做进一步更改:

public async Task<ActionResult> Register(RegisterViewModel model)
{
  if (ModelState.IsValid) {
    var user = new ApplicationUser() { UserName = model.UserName,
      MagicCode = model.MagicCode };
    var result = await UserManager.CreateAsync(user, model.Password);
    if (result.Succeeded) {
      await SignInAsync(user, isPersistent: false);
      return RedirectToAction("Index", "Home");
    }
}

关键更改是保存神奇代码的已发布数据(或要添加到用户定义的其他内容)。上述内容将保存到 ApplicationUser 实例,此实例会传递到 UserManager 对象的 CreateAsync 方法。

深入了解持久性存储

在 ASP.NET MVC 模板生成的示例代码中,AccountController 类具有在此处定义的成员:

public UserManager<ApplicationUser> UserManager { get; private set; }

将 UserManager 类实例化并传递用户存储对象。ASP.NET 标识随附了默认用户存储:

var defaultUserStore = new UserStore<ApplicationUser>(new ApplicationDbContext())

与 ApplicationUser 类似,ApplicationDbContext 类继承自系统定义的类(名为 IdentityDbContext),并对实体框架进行包装,以便执行实际的持久性作业。好消息是您可以去掉全部的默认存储机制,以便自己进行设置。您可以使自定义存储引擎基于 SQL Server 和 Entity Framework,而使用其他架构。您还可以利用完全不同的存储引擎,如 MySQL 或 NoSQL 解决方案。让我们来了解如何基于 RavenDB 的嵌入式版本 (ravendb.net) 来安排用户存储。您所需的类原型如下所示:

public class RavenDbUserStore<TUser> :
  IUserStore<TUser>, IUserPasswordStore<TUser>
  where TUser : TypicalUser
{
  ...
}

如果您要支持登录、角色和声明,则需要实现更多的接口。对于最简单的工作解决方案,IUserStore 和 IUserPasswordStore 就足够了。Typical­User 类是我所创建的自定义类,用于尽可能保持与 ASP.NET 标识基础结构分离:

public class TypicalUser : IUser
{
  // IUser interface
  public String Id { get; set; }
  public String UserName { get; set; }
  // Other members
  public String Password { get; set; }
  public String MagicCode { get; set; }
}

用户类至少必须实现 IUser 接口。此接口包括两个成员 Id 和 UserName。您可能还要添加成员 Password。这是保存在 RavenDB 存档中的用户类。

通过 RavenDB.Embedded NuGet 包,将 RavenDB 支持添加到您的项目。在 global.asax 中,您还需要使用以下代码将数据库初始化:

private static IDocumentStore _instance;
public static IDocumentStore Initialize()
{
  _instance = new EmbeddableDocumentStore 
    { ConnectionStringName = "RavenDB" };
  _instance.Initialize();
  return _instance;
}

此连接字符串指向您应创建数据库的路径。在 ASP.NET Web 应用程序中,App_Data 下的子文件夹非常适用:

<add name="RavenDB" connectionString="DataDir = ~\App_Data\Ravendb" />

用户存储类包括 IUserStore 和 IUserPasswordStore 接口中的方法的代码。应用程序可以使用这些代码管理用户和相关密码。图 5 显示了存储实现。

图 5 基于 RavenDB 的最小工作用户存储

public class RavenDbUserStore<TUser> :
  IUserStore<TUser>, IUserPasswordStore<TUser>
  where TUser : TypicalUser
{
  private IDocumentSession DocumentSession { get; set; }
  public RavenDbUserStore()
  {
    DocumentSession = RavenDbConfig.Instance.OpenAsyncSession();
  }
  public Task CreateAsync(TUser user)
  {
    if (user == null)
      throw new ArgumentNullException();
    DocumentSession.Store(user);
    return Task.FromResult<Object>(null);
  }
  public Task<TUser> FindByIdAsync(String id)
  {
    if (String.IsNullOrEmpty(id))
      throw new ArgumentException();
    var user = DocumentSession.Load<TUser>(id);
    return Task.FromResult<TUser>(user);
  }
  public Task<TUser> FindByNameAsync(String userName)
  {
    if (string.IsNullOrEmpty(userName))
      throw new ArgumentException("Missing user name");
    var user = DocumentSession.Query<TUser>()
      .FirstOrDefault(u => u.UserName == userName);
    return Task.FromResult<TUser>(user);
  }
  public Task UpdateAsync(TUser user)
  {
    if (user != null)
      DocumentSession.Store(user);
    return Task.FromResult<Object>(null);
  }
  public Task DeleteAsync(TUser user)
  {
    if (user != null)
      DocumentSession.Delete(user);
    return Task.FromResult<Object>(null);
  }
  public void Dispose()
  {
    if (DocumentSession == null)
      return;
    DocumentSession.SaveChanges();
    DocumentSession.Dispose();
  }
  public Task SetPasswordHashAsync(TUser user, String passwordHash)
  {
    user.Password = passwordHash;
    return Task.FromResult<Object>(null);
  }
  public Task<String> GetPasswordHashAsync(TUser user)
  {
    var passwordHash = user.Password;
    return Task.FromResult<string>(passwordHash);
  }
  public Task<Boolean> HasPasswordAsync(TUser user)
  {
    var hasPassword = String.IsNullOrEmpty(user.Password);
    return Task.FromResult<Boolean>(hasPassword);
  }
 }
}

与 RavenDB 的任何交互通过打开和关闭文档存储会话进行传递。在 RavenDbUserStore 的构造函数中,打开会话并在存储对象的 Dispose 方法中将其关闭。但在关闭会话之前,需调用 SaveChanges 方法来保留所有遵循工作单元模式的挂起更改:

public void Dispose()
{
  if (DocumentSession == null)
    return;
  DocumentSession.SaveChanges();
  DocumentSession.Dispose();
}

要与 RavenDB 数据库协同工作的 API 相当简单。创建新用户所需的代码如下:

public Task CreateAsync(TUser user)
{
  if (user == null)
    throw new ArgumentNullException();
  DocumentSession.Store(user);
  return Task.FromResult<Object>(null);
}

要检索给定用户,请使用 Document­Session 对象上的 Query 方法:

var user = DocumentSession.Load<TUser>(id);

RavenDB 假定您保留的任何类都具有 Id 属性。如果没有,它将隐式创建该属性,以便您可以始终使用 Load 方法按 Id 来检索任何对象,无论该 ID 是您自己的 ID 还是系统生成的 ID。要按名称来检索用户,请使用 LINQ 语法来执行典型查询:

var user = DocumentSession
              .Query<TUser>()
              .FirstOrDefault(u => u.UserName == userName);

使用 ToList 来选择各种对象,并将其存储到可管理列表。处理密码同样很简单。RavenDB 以哈希格式存储密码,但在 RavenDB 模块外部管理哈希算法。实际上,SetPasswordHashAsync 方法已经接收了用户提供的密码哈希。

图 5 是用于设置与 ASP.NET 标识相兼容的 RavenDB 用户存储的完整源代码。在安装了 RavenDB 的嵌入式版本后,该代码足以使用户登录和退出。对于更为复杂的功能(如外部登录和帐户管理),则需要实现所有 ASP.NET 标识用户相关接口或大量重写 Account­Controller 中从基架获得的代码。

底线

通过 ASP.NET 标识,您可以完全去掉基于实体框架的存储基础结构,并改为使用 RavenDB 这种无架构的文档数据库。您可以将 RavenDB 安装为 Windows 服务、IIS 应用程序或像我这样将其嵌入。只编写实现几个 ASP.NET 标识接口的类,并将此新存储类插入 UserManager 基础结构。甚至是在基于 EF 的默认配置中,更改用户类型架构也很简单。但如果您使用 RavenDB,请删除会引起用户格式更改的任何迁移问题。


Dino Esposito 是《Microsoft .NET:Architecting Applications for the Enterprise》(Microsoft Press,2014 年)和《Programming ASP.NET MVC 5》(Microsoft Press,2014 年)的合著者。作为 JetBrains 的 Microsoft .NET Framework 和 Android 平台的技术推广人员,Esposito 经常在全球行业活动中发表演讲,并在 software2cents.wordpress.com 上以及 twitter.com/despos 上的推文中分享他对于软件的愿景。

衷心感谢以下技术专家对本文的审阅:Mauro Servienti