将现有网站从 SQL 成员身份迁移到 ASP.NET Identity
作者 :Rick Anderson, Suhas Joshi
本教程说明将包含使用 SQL 成员身份创建的用户和角色数据的现有 Web 应用程序迁移到新的 ASP.NET 标识系统的步骤。 此方法涉及将现有数据库架构更改为 ASP.NET 标识所需的架构,并将旧/新类中的架构挂钩到该架构。 采用此方法后,迁移数据库后,将毫不费力地处理标识的未来更新。
在本教程中,我们将采用 (Web Forms) 使用 Visual Studio 2010 创建的 Web 应用程序模板来创建用户和角色数据。 然后,我们将使用 SQL 脚本将现有数据库迁移到标识系统所需的表。 接下来,我们将安装必要的 NuGet 包,并添加使用标识系统进行成员身份管理的新帐户管理页。 作为迁移测试,使用 SQL 成员身份创建的用户应能够登录,新用户应能够注册。 可在此处找到完整示例。 另请参阅 从 ASP.NET 成员身份迁移到 ASP.NET 标识。
入门
创建具有 SQL 成员身份的应用程序
我们需要从使用 SQL 成员身份并具有用户和角色数据的现有应用程序开始。 在本文中,让我们在 Visual Studio 2010 中创建一个 Web 应用程序。
使用 ASP.NET 配置工具创建 2 个用户: oldAdminUser 和 oldUser。
创建名为 管理员 的角色,并将“oldAdminUser”添加为该角色中的用户。
使用 Default.aspx 创建网站的 管理员 节。 在 web.config 文件中设置授权标记,以便仅允许具有管理员角色的用户进行访问。 可在此处找到详细信息 https://www.asp.net/web-forms/tutorials/security/roles/role-based-authorization-cs
在服务器资源管理器中查看数据库,了解 SQL 成员身份系统创建的表。 用户登录数据存储在aspnet_Users和aspnet_Membership表中,而角色数据存储在aspnet_Roles表中。 有关哪些用户的角色存储在 aspnet_UsersInRoles 表中的信息。 对于基本成员身份管理,将上表中的信息移植到 ASP.NET 标识系统就足够了。
迁移到 Visual Studio 2013
安装 Visual Studio Express 2013 for Web 或 Visual Studio 2013 以及最新更新。
在已安装的 Visual Studio 版本中打开上述项目。 如果计算机上未安装 SQL Server Express,则打开项目时会显示提示,因为连接字符串使用 SQL Express。 可以选择安装 SQL Express,也可以将连接字符串更改为 LocalDb。 在本文中,我们将它更改为 LocalDb。
打开web.config,并从 更改连接字符串。SQLExpress (LocalDb) v11.0。 从连接字符串中删除“User Instance=true”。
打开服务器资源管理器,验证表架构和数据是否可以观察到。
ASP.NET 标识系统适用于框架 4.5 或更高版本。 将应用程序重定向到 4.5 或更高版本。
生成项目以验证是否没有错误。
安装 Nuget 包
在“解决方案资源管理器”中,右键单击项目>“管理 NuGet 包”。 在搜索框中,输入“Asp.net 标识”。 在结果列表中选择包,然后单击“安装”。 单击“我接受”按钮接受许可协议。 请注意,此包将安装依赖项包:EntityFramework 和 Microsoft ASP.NET Identity Core。 同样,如果不想启用 OAuth 登录) ,请安装以下包 (跳过最后 4 个 OWIN 包:
Microsoft.AspNet.Identity.Owin
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security.Facebook
Microsoft.Owin.Security.Google
Microsoft.Owin.Security.MicrosoftAccount
Microsoft.Owin.Security.Twitter
将数据库迁移到新的标识系统
下一步是将现有数据库迁移到 ASP.NET 标识系统所需的架构。 为此,我们运行了一个 SQL 脚本,其中包含一组用于创建新表并将现有用户信息迁移到新表的命令。 可 在此处找到脚本文件。
此脚本文件特定于此示例。 如果自定义或修改了使用 SQL 成员身份创建的表的架构,则需要相应地更改脚本。
如何生成用于架构迁移的 SQL 脚本
若要 ASP.NET Identity 类现成地使用现有用户的数据,我们需要将数据库架构迁移到 ASP.NET Identity 所需的架构。 为此,可以添加新表并将现有信息复制到这些表。 默认情况下,ASP.NET Identity 使用 EntityFramework 将标识模型类映射回数据库以存储/检索信息。 这些模型类实现定义用户和角色对象的核心标识接口。 数据库中的表和列基于这些模型类。 Identity v2.1.0 中的 EntityFramework 模型类及其属性的定义如下
IdentityUser | 类型 | IdentityRole | IdentityUserRole | IdentityUserLogin | IdentityUserClaim |
---|---|---|---|---|---|
ID | 字符串 | ID | RoleId | ProviderKey | ID |
用户名 | string | 名称 | UserId | UserId | ClaimType |
PasswordHash | 字符串 | LoginProvider | ClaimValue | ||
SecurityStamp | 字符串 | User_Id | |||
电子邮件 | 字符串 | ||||
EmailConfirmed | bool | ||||
PhoneNumber | 字符串 | ||||
PhoneNumberConfirmed | bool | ||||
LockoutEnabled | bool | ||||
LockoutEndDate | DateTime | ||||
AccessFailedCount | int |
我们需要为其中每个模型提供与属性对应的列的表。 类和表之间的映射在 的 IdentityDBContext
方法中OnModelCreating
定义。 这称为 Fluent API 的配置方法,有关详细信息,可 在此处找到。 类的配置如下所述
类 | 表 | 主密钥 | 外键 |
---|---|---|---|
IdentityUser | AspnetUsers | ID | |
IdentityRole | AspnetRoles | ID | |
IdentityUserRole | AspnetUserRole | UserId + RoleId | User_Id-AspnetUsers> RoleId-AspnetRoles> |
IdentityUserLogin | AspnetUserLogins | ProviderKey+UserId + LoginProvider | UserId-AspnetUsers> |
IdentityUserClaim | AspnetUserClaims | ID | User_Id-AspnetUsers> |
利用此信息,我们可以创建 SQL 语句来创建新表。 我们可以单独编写每个语句,也可以使用 EntityFramework PowerShell 命令生成整个脚本,然后根据需要对其进行编辑。 为此,请在 VS 中从“视图”或“工具”菜单打开包管理器控制台
- 运行命令“Enable-Migrations”以启用 EntityFramework 迁移。
- 运行命令“Add-migration initial”,该命令创建初始安装代码以使用 C#/VB 创建数据库。
- 最后一步是运行“Update-Database –Script”命令,该命令基于模型类生成 SQL 脚本。
如果应用使用 SQLite 作为其标识数据存储,则不支持某些命令。 由于数据库引擎的限制,
Alter
命令会引发以下异常:“System.NotSupportedException: SQLite 不支持此迁移操作。”
解决方法是,对数据库运行 Code First 迁移以更改表。
此数据库生成脚本可用作开始,我们将进行其他更改以添加新列和复制数据。 这样做的优点是生成表, _MigrationHistory
当模型类针对标识版本的将来版本发生更改时,EntityFramework 使用该表来修改数据库架构。
除了标识用户模型类中的属性外,SQL 成员身份用户信息还具有其他属性,即电子邮件、密码尝试、上次登录日期、上次锁定日期等。这是有用的信息,我们希望将其转交给标识系统。 这可以通过向用户模型添加其他属性并将其映射回数据库中的表列来完成。 为此,可以添加一个子类来 IdentityUser
对模型进行子类。 我们可以将属性添加到此自定义类,并编辑 SQL 脚本,以在创建表时添加相应的列。 本文进一步介绍了此类的代码。 添加新属性后用于创建 AspnetUsers
表的 SQL 脚本为
CREATE TABLE [dbo].[AspNetUsers] (
[Id] NVARCHAR (128) NOT NULL,
[UserName] NVARCHAR (MAX) NULL,
[PasswordHash] NVARCHAR (MAX) NULL,
[SecurityStamp] NVARCHAR (MAX) NULL,
[EmailConfirmed] BIT NOT NULL,
[PhoneNumber] NVARCHAR (MAX) NULL,
[PhoneNumberConfirmed] BIT NOT NULL,
[TwoFactorEnabled] BIT NOT NULL,
[LockoutEndDateUtc] DATETIME NULL,
[LockoutEnabled] BIT NOT NULL,
[AccessFailedCount] INT NOT NULL,
[ApplicationId] UNIQUEIDENTIFIER NOT NULL,
[LegacyPasswordHash] NVARCHAR (MAX) NULL,
[LoweredUserName] NVARCHAR (256) NOT NULL,
[MobileAlias] NVARCHAR (16) DEFAULT (NULL) NULL,
[IsAnonymous] BIT DEFAULT ((0)) NOT NULL,
[LastActivityDate] DATETIME2 NOT NULL,
[MobilePIN] NVARCHAR (16) NULL,
[Email] NVARCHAR (256) NULL,
[LoweredEmail] NVARCHAR (256) NULL,
[PasswordQuestion] NVARCHAR (256) NULL,
[PasswordAnswer] NVARCHAR (128) NULL,
[IsApproved] BIT NOT NULL,
[IsLockedOut] BIT NOT NULL,
[CreateDate] DATETIME2 NOT NULL,
[LastLoginDate] DATETIME2 NOT NULL,
[LastPasswordChangedDate] DATETIME2 NOT NULL,
[LastLockoutDate] DATETIME2 NOT NULL,
[FailedPasswordAttemptCount] INT NOT NULL,
[FailedPasswordAttemptWindowStart] DATETIME2 NOT NULL,
[FailedPasswordAnswerAttemptCount] INT NOT NULL,
[FailedPasswordAnswerAttemptWindowStart] DATETIME2 NOT NULL,
[Comment] NTEXT NULL,
CONSTRAINT [PK_dbo.AspNetUsers] PRIMARY KEY CLUSTERED ([Id] ASC),
FOREIGN KEY ([ApplicationId]) REFERENCES [dbo].[aspnet_Applications] ([ApplicationId]),
);
接下来,我们需要将现有信息从 SQL 成员身份数据库复制到新添加的标识表。 这可以通过 SQL 将数据直接从一个表复制到另一个表来完成。 若要将数据添加到表的行中,请使用 INSERT INTO [Table]
构造。 若要从另一个表复制, INSERT INTO
可以将 语句与 语句一起使用 SELECT
。 若要获取所有用户信息,我们需要查询 aspnet_Users 和 aspnet_Membership 表,并将数据复制到 AspNetUsers 表。 我们将 使用 INSERT INTO
和 SELECT
以及 JOIN
和 LEFT OUTER JOIN
语句。 有关在表之间查询和复制数据的详细信息,请参阅 此 链接。 此外,AspnetUserLogins 和 AspnetUserClaims 表一开始为空,因为默认情况下 SQL 成员身份中没有映射到此信息的信息。 复制的信息仅针对用户和角色。 对于在前面的步骤中创建的项目,用于将信息复制到用户表的 SQL 查询将为
INSERT INTO AspNetUsers(Id,UserName,PasswordHash,SecurityStamp,EmailConfirmed,
PhoneNumber,PhoneNumberConfirmed,TwoFactorEnabled,LockoutEndDateUtc,LockoutEnabled,AccessFailedCount,
ApplicationId,LoweredUserName,MobileAlias,IsAnonymous,LastActivityDate,LegacyPasswordHash,
MobilePIN,Email,LoweredEmail,PasswordQuestion,PasswordAnswer,IsApproved,IsLockedOut,CreateDate,
LastLoginDate,LastPasswordChangedDate,LastLockoutDate,FailedPasswordAttemptCount,
FailedPasswordAnswerAttemptWindowStart,FailedPasswordAnswerAttemptCount,FailedPasswordAttemptWindowStart,Comment)
SELECT aspnet_Users.UserId,aspnet_Users.UserName,(aspnet_Membership.Password+'|'+CAST(aspnet_Membership.PasswordFormat as varchar)+'|'+aspnet_Membership.PasswordSalt),NewID(),
'true',NULL,'false','true',aspnet_Membership.LastLockoutDate,'true','0',
aspnet_Users.ApplicationId,aspnet_Users.LoweredUserName,
aspnet_Users.MobileAlias,aspnet_Users.IsAnonymous,aspnet_Users.LastActivityDate,aspnet_Membership.Password,
aspnet_Membership.MobilePIN,aspnet_Membership.Email,aspnet_Membership.LoweredEmail,aspnet_Membership.PasswordQuestion,aspnet_Membership.PasswordAnswer,
aspnet_Membership.IsApproved,aspnet_Membership.IsLockedOut,aspnet_Membership.CreateDate,aspnet_Membership.LastLoginDate,aspnet_Membership.LastPasswordChangedDate,
aspnet_Membership.LastLockoutDate,aspnet_Membership.FailedPasswordAttemptCount, aspnet_Membership.FailedPasswordAnswerAttemptWindowStart,
aspnet_Membership.FailedPasswordAnswerAttemptCount,aspnet_Membership.FailedPasswordAttemptWindowStart,aspnet_Membership.Comment
FROM aspnet_Users
LEFT OUTER JOIN aspnet_Membership ON aspnet_Membership.ApplicationId = aspnet_Users.ApplicationId
AND aspnet_Users.UserId = aspnet_Membership.UserId;
在上述 SQL 语句中,有关 aspnet_Users 和 aspnet_Membership 表中的每个用户的信息将复制到 AspnetUsers 表的列中。 此处完成的唯一修改是复制密码。 由于 SQL 成员身份中密码的加密算法使用了“PasswordSalt”和“PasswordFormat”,因此我们也复制了哈希密码,以便它可用于通过标识解密密码。 本文在挂接自定义密码时对此进行了进一步介绍。
此脚本文件特定于此示例。 对于具有其他表的应用程序,开发人员可以遵循类似的方法在用户模型类上添加其他属性,并将其映射到 AspnetUsers 表中的列。 若要运行脚本,
打开 Server Explorer。 展开“ApplicationServices”连接以显示表。 右键单击“表”节点,然后选择“新建查询”选项
在查询窗口中,复制并粘贴 Migrations.sql 文件中的整个 SQL 脚本。 按“执行”箭头按钮运行脚本文件。
刷新“服务器资源管理器”窗口。 在数据库中创建了五个新表。
下面介绍如何将 SQL 成员身份表中的信息映射到新的标识系统。
aspnet_Roles --> AspNetRoles
asp_netUsers和asp_netMembership --> AspNetUsers
aspnet_UserInRoles --> AspNetUserRoles
如上一部分所述,AspNetUserClaims 和 AspNetUserLogins 表为空。 AspNetUser 表中的“鉴别器”字段应与定义为下一步的模型类名称匹配。 此外,PasswordHash 列的格式为“encrypted password |password salt|password format”。 这使你可以使用特殊的 SQL 成员身份加密逻辑,以便可以重复使用旧密码。 本文稍后将对此进行说明。
创建模型和成员资格页
如前所述,默认情况下,标识功能使用实体框架与数据库通信以存储帐户信息。 若要处理表中的现有数据,我们需要创建映射回表的模型类,并将其挂接到标识系统中。 作为标识协定的一部分,模型类应实现 Identity.Core dll 中定义的接口,或者可以扩展 Microsoft.AspNet.Identity.EntityFramework 中提供的这些接口的现有实现。
在我们的示例中,AspNetRoles、AspNetUserClaims、AspNetLogins 和 AspNetUserRole 表具有类似于标识系统现有实现的列。 因此,我们可以重复使用现有类来映射到这些表。 AspNetUser 表具有一些其他列,用于存储 SQL 成员资格表中的其他信息。 这可以通过创建一个模型类来映射,该类扩展了“IdentityUser”的现有实现并添加其他属性。
在项目中创建 Models 文件夹并添加类 User。 类的名称应与在“AspnetUsers”表的“鉴别器”列中添加的数据匹配。
User 类应扩展 在 Microsoft.AspNet.Identity.EntityFramework dll 中找到的 IdentityUser 类。 声明类中映射回 AspNetUser 列的属性。 属性 ID、Username、PasswordHash 和 SecurityStamp 在 IdentityUser 中定义,因此省略。 下面是具有所有属性的 User 类的代码
public class User : IdentityUser { public User() { CreateDate = DateTime.Now; IsApproved = false; LastLoginDate = DateTime.Now; LastActivityDate = DateTime.Now; LastPasswordChangedDate = DateTime.Now; LastLockoutDate = DateTime.Parse("1/1/1754"); FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1/1/1754"); FailedPasswordAttemptWindowStart = DateTime.Parse("1/1/1754"); } public System.Guid ApplicationId { get; set; } public string MobileAlias { get; set; } public bool IsAnonymous { get; set; } public System.DateTime LastActivityDate { get; set; } public string MobilePIN { get; set; } public string LoweredEmail { get; set; } public string LoweredUserName { get; set; } public string PasswordQuestion { get; set; } public string PasswordAnswer { get; set; } public bool IsApproved { get; set; } public bool IsLockedOut { get; set; } public System.DateTime CreateDate { get; set; } public System.DateTime LastLoginDate { get; set; } public System.DateTime LastPasswordChangedDate { get; set; } public System.DateTime LastLockoutDate { get; set; } public int FailedPasswordAttemptCount { get; set; } public System.DateTime FailedPasswordAttemptWindowStart { get; set; } public int FailedPasswordAnswerAttemptCount { get; set; } public System.DateTime FailedPasswordAnswerAttemptWindowStart { get; set; } public string Comment { get; set; } }
需要 Entity Framework DbContext 类才能将模型中的数据保存回表,并从表检索数据以填充模型。 Microsoft.AspNet.Identity.EntityFramework dll 定义 IdentityDbContext 类,该类与标识表交互以检索和存储信息。 IdentityDbContext<tuser> 采用“TUser”类,该类可以是扩展 IdentityUser 类的任何类。
创建一个新类 ApplicationDBContext,该类在“Models”文件夹下扩展 IdentityDbContext,传入步骤 1 中创建的“User”类
public class ApplicationDbContext : IdentityDbContext<User> { }
新标识系统中的用户管理是使用 Microsoft.AspNet.Identity.EntityFramework dll 中定义的 UserManager<tuser> 类完成的。 我们需要创建一个扩展 UserManager 的自定义类,并传入步骤 1 中创建的“User”类。
在 Models 文件夹中创建一个新类 UserManager,用于扩展 UserManager<用户>
public class UserManager : UserManager<User> { }
应用程序用户的密码将加密并存储在数据库中。 SQL 成员身份中使用的加密算法不同于新标识系统中的加密算法。 若要重复使用旧密码,需要在旧用户使用 SQL 成员身份算法登录时选择性地解密密码,同时对新用户使用标识中的加密算法。
UserManager 类具有属性“PasswordHasher”,该属性存储实现“IPasswordHasher”接口的类的实例。 这用于在用户身份验证事务期间加密/解密密码。 在步骤 3 中定义的 UserManager 类中,创建新的类 SQLPasswordHasher 并复制以下代码。
public class SQLPasswordHasher : PasswordHasher { public override string HashPassword(string password) { return base.HashPassword(password); } public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) { string[] passwordProperties = hashedPassword.Split('|'); if (passwordProperties.Length != 3) { return base.VerifyHashedPassword(hashedPassword, providedPassword); } else { string passwordHash = passwordProperties[0]; int passwordformat = 1; string salt = passwordProperties[2]; if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase)) { return PasswordVerificationResult.SuccessRehashNeeded; } else { return PasswordVerificationResult.Failed; } } } //This is copied from the existing SQL providers and is provided only for back-compat. private string EncryptPassword(string pass, int passwordFormat, string salt) { if (passwordFormat == 0) // MembershipPasswordFormat.Clear return pass; byte[] bIn = Encoding.Unicode.GetBytes(pass); byte[] bSalt = Convert.FromBase64String(salt); byte[] bRet = null; if (passwordFormat == 1) { // MembershipPasswordFormat.Hashed HashAlgorithm hm = HashAlgorithm.Create("SHA1"); if (hm is KeyedHashAlgorithm) { KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm; if (kha.Key.Length == bSalt.Length) { kha.Key = bSalt; } else if (kha.Key.Length < bSalt.Length) { byte[] bKey = new byte[kha.Key.Length]; Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length); kha.Key = bKey; } else { byte[] bKey = new byte[kha.Key.Length]; for (int iter = 0; iter < bKey.Length; ) { int len = Math.Min(bSalt.Length, bKey.Length - iter); Buffer.BlockCopy(bSalt, 0, bKey, iter, len); iter += len; } kha.Key = bKey; } bRet = kha.ComputeHash(bIn); } else { byte[] bAll = new byte[bSalt.Length + bIn.Length]; Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length); Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length); bRet = hm.ComputeHash(bAll); } } return Convert.ToBase64String(bRet); }
通过导入 System.Text 和 System.Security.Cryptography 命名空间来解决编译错误。
EncodePassword 方法根据默认的 SQL 成员身份加密实现来加密密码。 这取自 System.Web dll。 如果旧应用使用了自定义实现,则应在此处反映它。 我们需要定义两个其他方法 HashPassword 和 VerifyHashedPassword ,这些方法使用 EncodePassword 方法对给定密码进行哈希处理,或使用数据库中存在的密码验证纯文本密码。
SQL 成员身份系统使用 PasswordHash、PasswordSalt 和 PasswordFormat 对用户注册或更改密码时输入的密码进行哈希处理。 在迁移过程中,所有三个字段都存储在 AspNetUser 表中的 PasswordHash 列中,用“|”字符分隔。 当用户登录并且密码具有这些字段时,我们使用 SQL 成员身份加密来检查密码;否则,我们使用标识系统的默认加密来验证密码。 这样,在迁移应用后,旧用户就不必更改其密码。
声明 UserManager 类的构造函数,并将其作为 SQLPasswordHasher 传递给构造函数中的 属性。
public UserManager() : base(new UserStore<User>(new ApplicationDbContext())) { this.PasswordHasher = new SQLPasswordHasher(); }
创建新的帐户管理页
迁移的下一步是添加允许用户注册和登录的帐户管理页。 SQL 成员资格中的旧帐户页使用不适用于新标识系统的控件。 若要添加新的用户管理页面,请从“为用户注册应用程序添加Web Forms”步骤开始,在此链接https://www.asp.net/identity/overview/getting-started/adding-aspnet-identity-to-an-empty-or-existing-web-forms-project处执行教程,因为我们已经创建了项目并添加了 NuGet 包。
我们需要对示例进行一些更改才能使用此处的项目。
类隐藏的 Register.aspx.cs 和 Login.aspx.cs 代码使用
UserManager
标识包中的 来创建用户。 对于此示例,请按照前面提到的步骤使用在 Models 文件夹中添加的 UserManager。使用创建的 User 类,而不是 Register.aspx.cs 中的 IdentityUser 和类隐藏的 Login.aspx.cs 代码。 这会将自定义用户类中的挂钩到标识系统。
可以跳过用于创建数据库的部件。
开发人员需要为新用户设置 ApplicationId 以匹配当前应用程序 ID。 为此,可以在 Register.aspx.cs 类中创建用户对象之前查询此应用程序的 ApplicationId,并在创建用户之前对其进行设置。
示例:
在 Register.aspx.cs 页中定义方法以查询aspnet_Applications表并根据应用程序名称获取应用程序 ID
private Guid GetApplicationID() { using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ApplicationServices"].ConnectionString)) { string queryString = "SELECT ApplicationId from aspnet_Applications WHERE ApplicationName = '/'"; //Set application name as in database SqlCommand command = new SqlCommand(queryString, connection); command.Connection.Open(); var reader = command.ExecuteReader(); while (reader.Read()) { return reader.GetGuid(0); } return Guid.NewGuid(); } }
现在,在用户对象上设置此项
var currentApplicationId = GetApplicationID(); User user = new User() { UserName = Username.Text, ApplicationId=currentApplicationId, …};
使用旧用户名和密码登录现有用户。 使用“注册”页创建新用户。 此外,请验证用户是否属于预期角色。
移植到标识系统可帮助用户将开放身份验证 (OAuth) 添加到应用程序。 请参阅 此处 启用了 OAuth 的示例。
后续步骤
在本教程中,我们展示了如何将用户从 SQL 成员身份移植到 ASP.NET 标识,但未移植配置文件数据。 在下一教程中,我们将探讨将配置文件数据从 SQL 成员身份移植到新的标识系统。
可以在本文底部留下反馈。
感谢汤姆·戴克斯特拉和里克·安德森对这篇文章的评论。