ASP.NET Identity 的自定义存储提供程序概述

作者 Tom FitzMacken

ASP.NET Identity 是一个可扩展的系统,可用于创建自己的存储提供程序并将其插入应用程序,而无需重新运行应用程序。 本主题介绍如何为 ASP.NET 标识创建自定义存储提供程序。 它涵盖了创建自己的存储提供程序的重要概念,但它不是实现自定义存储提供程序的分步演练。

有关实现自定义存储提供程序的示例,请参阅 实现自定义 MySQL ASP.NET 标识存储提供程序

本主题已针对 ASP.NET Identity 2.0 进行了更新。

本教程中使用的软件版本

  • 使用 Update 2 Visual Studio 2013
  • ASP.NET 标识 2

简介

默认情况下,ASP.NET 标识系统将用户信息存储在SQL Server数据库中,并使用 Entity Framework Code First 创建数据库。 对于许多应用程序,此方法非常有效。 但是,你可能更愿意使用不同类型的持久性机制(例如 Azure 表存储),或者可能已有与默认实现非常不同的结构的数据库表。 在任一情况下,都可以为存储机制编写自定义提供程序,并将该提供程序插入应用程序。

ASP.NET 标识默认包含在许多Visual Studio 2013模板中。 可以通过 Microsoft AspNet Identity EntityFramework NuGet 包获取 ASP.NET 标识的更新。

本主题包含下列部分:

了解体系结构

ASP.NET 标识由称为经理和存储的类组成。 Manager 是应用程序开发人员用于在 ASP.NET 标识系统中执行操作(例如创建用户)的高级类。 存储是用于指定如何保存实体(如用户和角色)的较低级别类。 存储与持久性机制紧密耦合,但管理器与存储分离,这意味着你可以在不中断整个应用程序的情况下替换持久性机制。

下图显示了 Web 应用程序如何与管理器交互,存储如何与数据访问层交互。

显示 Web 应用程序如何与经理交互的关系图

若要为 ASP.NET 标识创建自定义存储提供程序,必须创建数据源、数据访问层以及与此数据访问层交互的存储类。 可以继续使用同一管理器 API 对用户执行数据操作,但现在该数据已保存到其他存储系统。

无需自定义管理器类,因为在创建新 UserManager 实例或 RoleManager 时,需要提供用户类的类型,并将存储类的实例作为参数传递。 此方法使你能够将自定义类插入到现有结构中。 你将在 重新配置应用程序以使用新的存储提供程序部分中了解如何使用自定义存储类实例化 UserManager 和 RoleManager。

了解存储的数据

若要实现自定义存储提供程序,必须了解用于 ASP.NET 标识的数据类型,并确定哪些功能与应用程序相关。

数据 说明
用户 网站的已注册用户。 包括用户 ID 和用户名。 如果用户使用特定于站点 (的凭据登录,而不是使用来自外部站点(如 Facebook) )的凭据登录,则可能包括哈希密码,以及用于指示用户凭据中是否有任何更改的安全标记。 可能还包括电子邮件地址、电话号码、是否启用了双重身份验证、当前登录失败次数以及帐户是否已锁定。
用户声明 一组语句 (或声明) 表示用户标识的用户。 可以实现的用户身份表达式大于通过角色实现的表达式。
用户登录名 有关外部身份验证提供程序的信息 (Facebook) 在用户登录时使用。
角色 站点的授权组。 包括角色 Id 和角色名称(如“管理员”或“员工”)。

创建数据访问层

本主题假定你熟悉将使用的持久性机制以及如何为该机制创建实体。 本主题不提供有关如何创建存储库或数据访问类的详细信息;相反,它提供有关使用 ASP.NET 标识时需要做出的设计决策的一些建议。

在为自定义应用商店提供商设计存储库时,你有很多自由度。 只需为打算在应用程序中使用的功能创建存储库。 例如,如果未在应用程序中使用角色,则无需为角色或用户角色创建存储。 技术和现有基础结构可能需要与 ASP.NET 标识的默认实现大不相同的结构。 在数据访问层中,提供用于处理存储库结构的逻辑。

有关 ASP.NET Identity 2.0 的数据存储库的 MySQL 实现,请参阅 MySQLIdentity.sql

在数据访问层中,提供用于将 ASP.NET Identity 中的数据保存到数据源的逻辑。 自定义存储提供程序的数据访问层可能包含以下类来存储用户和角色信息。

说明 示例
上下文 封装信息以连接到持久性机制并执行查询。 此类是数据访问层的核心。 其他数据类将需要此类的实例来执行其操作。 你还将使用此类的实例初始化存储类。 MySQLDatabase
用户存储 存储和检索用户信息(如用户名和密码哈希)。 UserTable (MySQL)
角色存储 存储和检索角色信息(如角色名称)。 RoleTable (MySQL)
UserClaims 存储 存储和检索角色声明信息(如声明类型和值)。 UserClaimsTable (MySQL)
UserLogins 存储 存储和检索用户登录信息(如外部身份验证提供程序)。 UserLoginsTable (MySQL)
UserRole 存储 存储和检索分配给用户的角色。 UserRoleTable (MySQL)

同样,只需实现打算在应用程序中使用的类。

在数据访问类中,提供代码来为特定的持久性机制执行数据操作。 例如,在 MySQL 实现中,UserTable 类包含用于将新记录插入 Users 数据库表的方法。 名为 的 _database 变量是 MySQLDatabase 类的实例。

public int Insert(TUser user)
{
    string commandText = @"Insert into Users (UserName, Id, PasswordHash, SecurityStamp,Email,EmailConfirmed,PhoneNumber,PhoneNumberConfirmed, AccessFailedCount,LockoutEnabled,LockoutEndDateUtc,TwoFactorEnabled)
        values (@name, @id, @pwdHash, @SecStamp,@email,@emailconfirmed,@phonenumber,@phonenumberconfirmed,@accesscount,@lockoutenabled,@lockoutenddate,@twofactorenabled)";
    Dictionary<string, object> parameters = new Dictionary<string, object>();
    parameters.Add("@name", user.UserName);
    parameters.Add("@id", user.Id);
    parameters.Add("@pwdHash", user.PasswordHash);
    parameters.Add("@SecStamp", user.SecurityStamp);
    parameters.Add("@email", user.Email);
    parameters.Add("@emailconfirmed", user.EmailConfirmed);
    parameters.Add("@phonenumber", user.PhoneNumber);
    parameters.Add("@phonenumberconfirmed", user.PhoneNumberConfirmed);
    parameters.Add("@accesscount", user.AccessFailedCount);
    parameters.Add("@lockoutenabled", user.LockoutEnabled);
    parameters.Add("@lockoutenddate", user.LockoutEndDateUtc);
    parameters.Add("@twofactorenabled", user.TwoFactorEnabled);

    return _database.Execute(commandText, parameters);
}

创建数据访问类后,必须创建调用数据访问层中特定方法的存储类。

自定义用户类

实现自己的存储提供程序时,必须创建一个用户类,该类等效于 Microsoft.ASP.NET.Identity.EntityFramework 命名空间中的 IdentityUser 类:

下图显示了必须创建的 IdentityUser 类和要在此类中实现的接口。

Identity User 类的图像

IUser<TKey> 接口定义 UserManager 在执行请求的操作时尝试调用的属性。 接口包含两个属性 - Id 和 UserName。 使用 IUser<TKey 接口,可以通过泛型 TKey> 参数为用户指定密钥的类型。 Id 属性的类型与 TKey 参数的值匹配。

如果要对键使用字符串值,标识框架还提供 IUser 接口 (,而无需) 泛型参数。

IdentityUser 类实现 IUser,并包含网站上用户的任何其他属性或构造函数。 以下示例演示一个 IdentityUser 类,该类使用整数作为键。 Id 字段设置为 int 以匹配泛型参数的值。

public class IdentityUser : IUser<int>
{
    public IdentityUser() { ... }
    public IdentityUser(string userName) { ... }
    public int Id { get; set; }
    public string UserName { get; set; }
    // can also define optional properties such as:
    //    PasswordHash
    //    SecurityStamp
    //    Claims
    //    Logins
    //    Roles
}

有关完整实现,请参阅 IdentityUser (MySQL)

自定义用户存储

还可以创建 UserStore 类,该类提供针对用户的所有数据操作的方法。 此类等效于 Microsoft.ASP.NET.Identity.EntityFramework 命名空间中的 UserStore<TUser> 类。 在 UserStore 类中,实现 IUserStore<TUser、TKey> 和任何可选接口。 根据希望在应用程序中提供的功能选择要实现的可选接口。

下图显示了必须创建的 UserStore 类和相关接口。

用户应用商店类的图像

Visual Studio 中的默认项目模板包含的代码假定许多可选接口已在用户存储中实现。 如果将默认模板与自定义用户存储一起使用,则必须在用户存储中实现可选接口,或更改模板代码以不再调用尚未实现的接口中的方法。

以下示例演示一个简单的用户存储类。 TUser 泛型参数采用用户类的类型,该类型通常为定义的 IdentityUser 类。 TKey 泛型参数采用用户密钥的类型。

public class UserStore : IUserStore<IdentityUser, int>
{
    public UserStore() { ... }
    public UserStore(ExampleStorage database) { ... }
    public Task CreateAsync(IdentityUser user) { ... }
    public Task DeleteAsync(IdentityUser user) { ... }
    public Task<IdentityUser> FindByIdAsync(int userId) { ... }
    public Task<IdentityUser> FindByNameAsync(string userName) { ... }
    public Task UpdateAsync(IdentityUser user) { ... }
    public void Dispose() { ... }
}

在此示例中,采用类型为 ExampleDatabase 的名为 database 的参数的构造函数只是有关如何传入数据访问类的插图。 例如,在 MySQL 实现中,此构造函数采用 MySQLDatabase 类型的参数。

在 UserStore 类中,使用创建的数据访问类来执行操作。 例如,在 MySQL 实现中,UserStore 类具有 CreateAsync 方法,该方法使用 UserTable 实例插入新记录。 userTable 对象上的 Insert 方法与上一节中显示的方法相同。

public Task CreateAsync(IdentityUser user)
{
    if (user == null) {
        throw new ArgumentNullException("user");
    }

    userTable.Insert(user);

    return Task.FromResult<object>(null);
}

自定义用户存储时要实现的接口

下图显示了有关每个接口中定义的功能的更多详细信息。 所有可选接口都继承自 IUserStore。

显示每个接口中定义的功能的更多详细信息的插图

  • IUserStore
    IUserStore<TUser, TKey> 接口是必须在用户存储中实现的唯一接口。 它定义用于创建、更新、删除和检索用户的方法。

  • IUserClaimStore
    IUserClaimStore<TUser, TKey> 接口定义必须在用户存储中实现以启用用户声明的方法。 它包含方法或添加、删除和检索用户声明。

  • IUserLoginStore
    IUserLoginStore<TUser, TKey> 定义必须在用户存储中实现以启用外部身份验证提供程序的方法。 它包含用于添加、删除和检索用户登录名的方法,以及用于基于登录信息检索用户的方法。

  • IUserRoleStore
    IUserRoleStore<TKey, TUser> 接口定义必须在用户存储中实现的方法,以便将用户映射到角色。 它包含用于添加、删除和检索用户角色的方法,以及用于检查是否将用户分配给角色的方法。

  • IUserPasswordStore
    IUserPasswordStore<TUser, TKey> 接口定义在用户存储中必须实现以持久保存哈希密码的方法。 它包含用于获取和设置哈希密码的方法,以及用于指示用户是否设置了密码的方法。

  • IUserSecurityStampStore
    IUserSecurityStampStore<TUser, TKey> 接口定义必须在用户存储中实现的方法,以使用安全标记来指示用户帐户信息是否已更改。 当用户更改密码或是添加或删除登录名时,此安全戳会更新。 它包含用于获取和设置安全戳的方法。

  • IUserTwoFactorStore
    IUserTwoFactorStore<TUser, TKey> 接口定义实现双重身份验证必须实现的方法。 它包含用于获取和设置是否为用户启用了双因素身份验证的方法。

  • IUserPhoneNumberStore
    IUserPhoneNumberStore<TUser, TKey> 接口定义存储用户电话号码时必须实现的方法。 它包含用于获取和设置电话号码以及是否确认了电话号码的方法。

  • IUserEmailStore
    IUserEmailStore<TUser, TKey> 接口定义存储用户电子邮件地址时必须实现的方法。 它包含用于获取和设置电子邮件地址以及是否确认了电子邮件的方法。

  • IUserLockoutStore
    IUserLockoutStore<TUser, TKey> 接口定义必须实现的方法,以存储有关锁定帐户的信息。 它包含用于获取当前失败的访问尝试次数、获取和设置帐户是否可以锁定、获取和设置锁定结束日期、递增失败尝试次数以及重置失败尝试次数的方法。

  • IQueryableUserStore
    IQueryableUserStore<TUser, TKey> 接口定义必须实现的成员以提供可查询的用户存储。 它包含一个属性,用于保存可查询用户。

    实现应用程序中所需的接口;例如,IUserClaimStore、IUserLoginStore、IUserRoleStore、IUserPasswordStore 和 IUserSecurityStampStore 接口,如下所示。

public class UserStore : IUserStore<IdentityUser, int>,
                         IUserClaimStore<IdentityUser, int>,
                         IUserLoginStore<IdentityUser, int>,
                         IUserRoleStore<IdentityUser, int>,
                         IUserPasswordStore<IdentityUser, int>,
                         IUserSecurityStampStore<IdentityUser, int>
{
    // interface implementations not shown
}

有关包括所有接口) 的完整实现 (,请参阅 UserStore (MySQL)

IdentityUserClaim、IdentityUserLogin 和 IdentityUserRole

Microsoft.AspNet.Identity.EntityFramework 命名空间包含 IdentityUserClaimIdentityUserLoginIdentityUserRole 类的实现。 如果使用这些功能,可能需要创建自己的这些类版本,并为应用程序定义属性。 但是,有时在执行基本操作(例如添加或删除用户声明 () )时,不将这些实体加载到内存中会更有效。 而后端存储类可以直接对数据源执行这些操作。 例如,UserStore.GetClaimsAsync () 方法可以调用 userClaimTable.FindByUserId (用户。ID) 方法,用于直接对该表执行查询并返回声明列表。

public Task<IList<Claim>> GetClaimsAsync(IdentityUser user)
{
    ClaimsIdentity identity = userClaimsTable.FindByUserId(user.Id);
    return Task.FromResult<IList<Claim>>(identity.Claims.ToList());
}

自定义角色类

实现自己的存储提供程序时,必须创建一个角色类,该角色类等效于 Microsoft.ASP.NET.Identity.EntityFramework 命名空间中的 IdentityRole 类:

下图显示了必须创建的 IdentityRole 类以及要在此类中实现的接口。

标识角色类的图像

IRole<TKey> 接口定义 RoleManager 在执行请求的操作时尝试调用的属性。 接口包含两个属性 - Id 和 Name。 使用 IRole<TKey> 接口,可以通过泛型 TKey 参数指定角色的密钥类型。 Id 属性的类型与 TKey 参数的值匹配。

如果要对键使用字符串值,标识框架还提供 IRole 接口 (,而无需) 泛型参数。

以下示例演示一个 IdentityRole 类,该类使用整数作为键。 Id 字段设置为 int 以匹配泛型参数的值。

public class IdentityRole : IRole<int>
{
    public IdentityRole() { ... }
    public IdentityRole(string roleName) { ... }
    public int Id { get; set; }
    public string Name { get; set; }
}

有关完整实现,请参阅 IdentityRole (MySQL)

自定义角色存储

还可以创建 RoleStore 类,该类提供针对角色的所有数据操作的方法。 此类等效于 Microsoft.ASP.NET.Identity.EntityFramework 命名空间中的 RoleStore<TRole> 类。 在 RoleStore 类中,实现 IRoleStore<TRole, TKey> ,并选择性地实现 IQueryableRoleStore<TRole, TKey> 接口。

显示角色存储类的图像

以下示例演示了角色存储类。 TRole 泛型参数采用角色类的类型,该类型通常是定义的 IdentityRole 类。 TKey 泛型参数采用角色密钥的类型。

public class RoleStore : IRoleStore<IdentityRole, int>
{
    public RoleStore() { ... }
    public RoleStore(ExampleStorage database) { ... }
    public Task CreateAsync(IdentityRole role) { ... }
    public Task DeleteAsync(IdentityRole role) { ... }
    public Task<IdentityRole> FindByIdAsync(int roleId) { ... }
    public Task<IdentityRole> FindByNameAsync(string roleName) { ... }
    public Task UpdateAsync(IdentityRole role) { ... }
    public void Dispose() { ... }
}
  • IRoleStore<TRole>
    IRoleStore 接口定义要在角色存储类中实现的方法。 它包含用于创建、更新、删除和检索角色的方法。

  • RoleStore<TRole>
    若要自定义 RoleStore,请创建实现 IRoleStore 接口的类。 仅当想要在系统上使用角色时,才需要实现此类。 采用类型为 ExampleDatabase 的名为 database 的参数的构造函数只是有关如何传入数据访问类的插图。 例如,在 MySQL 实现中,此构造函数采用 MySQLDatabase 类型的参数。

    有关完整实现,请参阅 RoleStore (MySQL)

重新配置应用程序以使用新的存储提供程序

你已实现新的存储提供程序。 现在,必须将应用程序配置为使用此存储提供程序。 如果项目中包含默认存储提供程序,则必须删除默认提供程序并将其替换为提供程序。

替换 MVC 项目中的默认存储提供程序

  1. “管理 NuGet 包” 窗口中,卸载 Microsoft ASP.NET Identity EntityFramework 包。 可以通过在 Identity.EntityFramework 的已安装包中搜索来查找此包。
    Nu Get 包窗口的图像 系统将询问你是否还要卸载实体框架。 如果应用程序的其他部分不需要它,可以将其卸载。

  2. 在“Models”文件夹的 IdentityModels.cs 文件中,删除或注释掉 ApplicationUserApplicationDbContext 类。 在 MVC 应用程序中,可以删除整个 IdentityModels.cs 文件。 在Web Forms应用程序中,删除这两个类,但请确保保留也位于 IdentityModels.cs 文件中的帮助程序类。

  3. 如果存储提供程序驻留在单独的项目中,请在 Web 应用程序中添加对它的引用。

  4. 对于存储提供程序的命名空间,将所有对 using Microsoft.AspNet.Identity.EntityFramework; 的引用替换为 using 语句。

  5. Startup.Auth.cs 类中,更改 ConfigureAuth 方法以使用相应上下文的单个实例。

    public void ConfigureAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext(ExampleStorageContext.Create);
        app.CreatePerOwinContext(ApplicationUserManager.Create);
        ...
    
  6. 在 App_Start 文件夹中,打开 IdentityConfig.cs。 在 ApplicationUserManager 类中,更改 Create 方法以返回使用自定义用户存储的用户管理器。

    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
    {
        var manager = new ApplicationUserManager(new UserStore(context.Get<ExampleStorageContext>()));
        ...
    }
    
  7. 将所有对 ApplicationUser 的 引用替换为 IdentityUser

  8. 默认项目包括用户类中的一些成员,这些成员未在 IUser 接口中定义;例如 Email、PasswordHash 和 GenerateUserIdentityAsync。 如果用户类没有这些成员,则必须实现它们或更改使用这些成员的代码。

  9. 如果已创建 RoleManager 的任何实例,请将该代码更改为使用新的 RoleStore 类。

    var roleManager = new RoleManager<IdentityRole>(new RoleStore(context.Get<ExampleStorageContext>()));
    
  10. 默认项目是为具有键字符串值的用户类设计的。 如果用户类具有不同的键 (类型(例如整数) ),则必须更改项目以使用你的类型。 请参阅 更改 ASP.NET 标识中的用户的主键

  11. 如果需要,请将连接字符串添加到Web.config文件。

其他资源