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

作者:Steve Smith

ASP.NET Core Identity是一种可扩展系统,可让你创建自定义存储提供程序并将它连接到你的应用。 本主题介绍了如何为 ASP.NET Core Identity创建自定义存储提供程序。 它介绍用于创建自己的存储提供程序的重要概念,但并不是分步演练。 请参阅Identity 模型自定义以自定义Identity模型。

简介

默认情况下,ASP.NET Core Identity系统使用 Entity Framework Core 将用户信息存储在 SQL Server 数据库中。 对于许多应用,此方法的效果很好。 但是,你可能希望使用不同的持久性机制或数据架构。 例如:

  • 使用 Azure 表存储或其他数据存储。
  • 数据库表具有不同的结构。
  • 你可能希望使用不同的数据访问方法,例如 Dapper

在以上每种情况下,都可以为存储机制编写自定义提供程序,并将该提供程序插入应用中。

ASP.NET Core Identity 包含在 Visual Studio 中的项目模板中,其中包含“个人帐户”选项。

使用 .NET CLI 时,添加 -au Individual

dotnet new mvc -au Individual

ASP.NET Core Identity体系结构

ASP.NET Core Identity由称为管理器和存储的组成。 管理器是应用开发人员用于执行操作(如创建 用户)的高级类。 存储是用于指定如何保存实体(如用户和角色)的较低级别类。 存储遵循存储库模式,并与持久性机制紧密耦合。 管理器与存储分离,这意味着,你可以在不更改应用程序代码的情况下替换持久性机制(配置除外)。

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

ASP.NET Core 应用与管理器(例如 UserManager、RoleManager)结合使用。管理器与存储(例如 UserStore)结合使用,存储使用 Entity Framework Core 等库与数据源进行通信。

若要创建自定义存储提供程序,请创建数据源、数据访问层以及与此数据访问层交互的存储类(上图中的绿色和灰色框)。 不需要自定义管理器或是与它们交互的应用代码(上面的蓝色框)。

创建 UserManagerRoleManager 的新实例时,提供用户类的类型并将存储类的实例作为参数进行传递。 此方法使你可以将自定义类插入 ASP.NET Core 中。

重新配置应用以使用新存储提供程序演示如何使用自定义存储实例化 UserManagerRoleManager

ASP.NET Core Identity存储数据类型

ASP.NET Core Identity数据类型在以下部分中进行了详细介绍:

用户

网站的已注册用户。 IdentityUser 类型可以进行扩展,也可以用作你自己的自定义类型示例。 无需从特定类型继承,才能实现自己的自定义标识存储解决方案。

用户声明

一组关于用户的语句(或 声明),用于表示用户的身份。 可以实现的用户身份表达式大于通过角色实现的表达式。

用户登录名

有关在使用户登录时要使用的外部身份验证提供程序(如 Facebook 或 Microsoft 帐户)的信息。 示例

角色

站点的授权组。 包括角色 Id 和角色名称(如“管理员”或“员工”)。 示例

数据访问层

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

为自定义存储提供程序设计数据访问层时,你可以十分自由。 你只需为打算在应用中使用的功能创建持久性机制。 例如,如果你未在应用中使用角色,则无需为角色或用户角色关联创建存储。 你的技术和现有基础结构可能需要与 ASP.NET CoreIdentity的默认实现非常不同的结构。 在数据访问层中,提供用于处理存储实现结构的逻辑。

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

Context 类

封装信息以连接到持久性机制并执行查询。 几个数据类需要此类的实例(通常通过依赖项注入来提供)。 示例

用户存储

存储和检索用户信息(如用户名和密码哈希)。 示例

角色存储

存储和检索角色信息(如角色名称)。 示例

UserClaims 存储

存储和检索角色声明信息(如声明类型和值)。 示例

UserLogins 存储

存储和检索用户登录信息(如外部身份验证提供程序)。 示例

UserRole 存储

存储和检索向哪些用户分配哪些角色。 示例

提示:仅实现打算在应用中使用的类。

在数据访问类中,提供代码来为持久性机制执行数据操作。 例如,在自定义提供程序中,可能具有以下代码,用于在存储类中创建新用户:

public async Task<IdentityResult> CreateAsync(ApplicationUser user, 
    CancellationToken cancellationToken = default(CancellationToken))
{
    cancellationToken.ThrowIfCancellationRequested();
    if (user == null) throw new ArgumentNullException(nameof(user));

    return await _usersTable.CreateAsync(user);
}

用于创建用户的实现逻辑位于 _usersTable.CreateAsync 方法中,如下所示。

自定义用户类

实现存储提供程序时,创建与 IdentityUser 类等效的用户类。

用户类必须至少包含 IdUserName 属性。

IdentityUser 类定义 UserManager 在执行请求的操作时调用的属性。 Id 属性的默认类型是字符串,不过可以从 IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken> 继承并指定不同类型。 框架需要存储实现来处理数据类型转换。

自定义用户存储

创建 UserStore 类,该类为针对用户执行的所有数据操作提供方法。 此类等效于UserStore<TUser>类。 在 UserStore 类中,实现 IUserStore<TUser> 和所需的可选接口。 基于应用中提供的功能选择要实现的可选接口。

可选接口

可选接口继承自 IUserStore<TUser>。 可以在示例应用中看到部分实现的示例用户存储。

UserStore 类中,可使用创建的数据访问类执行操作。 这些类使用依赖项注入进行传入。 例如,在具有 Dapper 实现的 SQL Server 中,UserStore 类具有 CreateAsync 方法,该方法使用 DapperUsersTable 的实例插入新记录:

public async Task<IdentityResult> CreateAsync(ApplicationUser user)
{
    string sql = "INSERT INTO dbo.CustomUser " +
        "VALUES (@id, @Email, @EmailConfirmed, @PasswordHash, @UserName)";

    int rows = await _connection.ExecuteAsync(sql, new { user.Id, user.Email, user.EmailConfirmed, user.PasswordHash, user.UserName });

    if(rows > 0)
    {
        return IdentityResult.Success;
    }
    return IdentityResult.Failed(new IdentityError { Description = $"Could not insert user {user.Email}." });
}

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

  • IUserStore
    IUserStore<TUser>接口是必须在用户存储中实现的唯一接口。 它定义用于创建、更新、删除和检索用户的方法。
  • IUserClaimStore
    IUserClaimStore<TUser>接口定义为启用用户声明而实现的方法。 它包含用于添加、删除和检索用户声明的方法。
  • IUserLoginStore
    IUserLoginStore<TUser>定义为启用外部身份验证提供程序而实现的方法。 它包含用于添加、删除和检索用户登录名的方法,以及用于基于登录信息检索用户的方法。
  • IUserRoleStore
    IUserRoleStore<TUser>接口定义为将用户映射到角色而实现的方法。 它包含用于添加、删除和检索用户角色的方法,以及用于检查是否将用户分配给角色的方法。
  • IUserPasswordStore
    IUserPasswordStore<TUser>接口定义为保存哈希密码而实现的方法。 它包含用于获取和设置哈希密码的方法,以及用于指示用户是否设置了密码的方法。
  • IUserSecurityStampStore
    IUserSecurityStampStore<TUser>接口定义为使用安全戳指示用户的帐户信息是否已更改而实现的方法。 当用户更改密码或是添加或删除登录名时,此安全戳会更新。 它包含用于获取和设置安全戳的方法。
  • IUserTwoFactorStore
    IUserTwoFactorStore<TUser>接口定义为支持双因素身份验证而实现的方法。 它包含用于获取和设置是否为用户启用了双因素身份验证的方法。
  • IUserPhoneNumberStore
    IUserPhoneNumberStore<TUser>接口定义为存储电话号码而实现的方法。 它包含用于获取和设置电话号码以及是否确认了电话号码的方法。
  • IUserEmailStore
    IUserEmailStore<TUser>接口定义为用户电子邮件地址而实现的方法。 它包含用于获取和设置电子邮件地址以及是否确认了电子邮件的方法。
  • IUserLockoutStore
    IUserLockoutStore<TUser>接口定义为存储有关锁定帐户的信息而实现的方法。 它包含用于跟踪失败访问尝试和锁定的方法。
  • IQueryableUserStore
    IQueryableUserStore<TUser>接口定义为提供可查询用户存储而实现的成员。

只需实现应用中所需的接口。 例如:

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

IdentityUserClaim、IdentityUserLogin 和 IdentityUserRole

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

自定义角色类

实现角色存储提供程序时,可以创建自定义角色类型。 它不需要实现特定接口,但必须具有 Id,并且通常具有 Name 属性。

下面是示例角色类:

using System;

namespace CustomIdentityProviderSample.CustomProvider
{
    public class ApplicationRole
    {
        public Guid Id { get; set; } = Guid.NewGuid();
        public string Name { get; set; }
    }
}

自定义角色存储

可以创建 RoleStore 类,该类为针对角色执行的所有数据操作提供方法。 此类等效于 RoleStore<TRole> 类。 在 RoleStore 类中,可实现 IRoleStore<TRole> 和(可选)IQueryableRoleStore<TRole> 接口。

  • IRoleStore<TRole>
    IRoleStore<TRole>接口定义要在角色存储类中实现的方法。 它包含用于创建、更新、删除和检索角色的方法。
  • RoleStore<TRole>
    若要自定义 RoleStore,请创建实现 IRoleStore<TRole> 接口的类。

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

实现存储提供程序后,可将应用配置为使用它。 如果应用使用了默认提供程序,请将它替换为自定义提供程序。

  1. 删除 Microsoft.AspNetCore.EntityFramework.Identity NuGet 包。
  2. 如果存储提供程序处于在单独的项目或包中,请添加对它的引用。
  3. 对于存储提供程序的命名空间,将所有对 Microsoft.AspNetCore.EntityFramework.Identity 的引用替换为 using 语句。
  4. 更改AddIdentity方法以使用自定义类型。 为此,你可以创建自己的扩展方法。 有关示例,请参阅 IdentityServiceCollectionExtensions
  5. 如果使用角色,请更新 RoleManager 以使用 RoleStore 类。
  6. 将连接字符串和凭据更新到应用配置。

警告

本文介绍连接字符串的使用。 使用本地数据库时,用户无需进行身份验证,但在生产环境中,连接字符串有时包括进行身份验证的密码。 资源所有者密码凭据(ROPC)是在生产数据库中应避免的安全风险。 生产应用应使用可用的最安全的身份验证流。 有关部署到测试或生产环境的应用的身份验证的详细信息,请参阅 安全身份验证流

示例:

public void ConfigureServices(IServiceCollection services)
{
    // Add identity types
    services.AddIdentity<ApplicationUser, ApplicationRole>()
        .AddDefaultTokenProviders();

    // Identity Services
    services.AddTransient<IUserStore<ApplicationUser>, CustomUserStore>();
    services.AddTransient<IRoleStore<ApplicationRole>, CustomRoleStore>();
    string connectionString = Configuration.GetConnectionString("DefaultConnection");
    services.AddTransient<SqlConnection>(e => new SqlConnection(connectionString));
    services.AddTransient<DapperUsersTable>();

    // additional configuration
}
var builder = WebApplication.CreateBuilder(args);

// Add identity types
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddDefaultTokenProviders();

// Identity Services
builder.Services.AddTransient<IUserStore<ApplicationUser>, CustomUserStore>();
builder.Services.AddTransient<IRoleStore<ApplicationRole>, CustomRoleStore>();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddTransient<SqlConnection>(e => new SqlConnection(connectionString));
builder.Services.AddTransient<DapperUsersTable>();

// additional configuration

builder.Services.AddRazorPages();

var app = builder.Build();

参考