ASP.NET Identity 2.0 扩展身份模型并使用整数键而不是字符串
深入探讨扩展 Identity 2.0 框架提供的核心模型集,并使用整数键而非默认字符串键重新实现基本的 Identity 示例项目。
ASP.NET Identity 框架于 2014 年 3 月 20 日发布,带来了大量期待已久的增强功能,为 ASP.NET 开发人员社区提供了一个完整的身份验证和授权平台。
在之前的文章中,我们广泛探讨了新框架的结构以及它与 1.0 版本的不同之处。我们还逐步实现了电子邮件帐户确认和双因素身份验证,并扩展了基本的 User 和 Role 模型(这比你想象的要费力一些)。
在这篇文章中,我们将深入探讨扩展 Identity 2.0 框架提供的核心模型集,并使用整数键而非默认字符串键重新实现基本的 Identity 示例项目。
Github 上的源代码
在本文中,我们将基本上使用整数键重新实现 Identity Samples 项目。你可以从我的 Github 仓库克隆完整的源代码。此外,如果你发现错误和/或有建议,请务必提出问题和/或向我发送拉取请求!
更新:我还创建了一个即用型易于扩展的 Identity 2.0 项目模板,可用于创建一个 Identity 2.0 项目,其中所有基本的 Identity 模型都可以轻松扩展,而无需每次都对泛型类型参数进行大量调整。
- ASP.NET Identity 最初为何使用字符串键?
- Identity 2.0 核心类使用泛型类型参数
- 使用 Identity 2.0 和 Identity 示例项目实现整数键
- 重新设计基本身份模型
- Cookie 身份验证配置
- 更新 Admin 视图模型
- 更新控制器方法参数
- 将整数类型参数添加到 GetUserId() 调用
- 更新角色管理视图
- 关于安全性的说明
ASP.NET Identity 最初为何使用字符串键?
一个流行且有些令人困惑的问题是:“Identity 团队为什么选择字符串键作为 Identity 框架模型的默认值?”我们许多使用数据库长大的人倾向于使用简单的自增整数作为数据库主键,因为它简单,并且至少在理论上,在表索引等方面有一些性能优势。
Identity 团队决定使用字符串作为键的决定在 Microsoft ASP.NET 撰稿人Rick Anderson的Stack Overflow 答案中得到了最好的总结
- Identity 运行时倾向于将字符串用于用户 ID,因为我们不想费心去找出用户 ID 的正确序列化方式(出于同样的原因,我们也对 Claims 使用字符串),例如,所有(或大多数)Identity 接口都将用户 ID 引用为字符串。
- 那些自定义持久层(例如实体类型)的人可以选择他们想要的任何类型作为键,但他们需要负责向我们提供键的字符串表示形式。
- 默认情况下,我们为每个新用户使用 GUID 的字符串表示形式,但这只是因为它为我们提供了一种非常简单的方式来自动生成唯一 ID。
这个决定并非没有社区中的反对者。上面描述的默认字符串键本质上是 Guid 的字符串表示形式。正如Reddit 上的讨论所示,关于它与关系数据库后端相比的性能方面存在争议。
Reddit 讨论中提到的担忧主要集中在数据库索引性能上,对于大量小型站点和 Web 应用程序,特别是对于学习项目和学生来说,这不太可能成为问题。然而,正如前面所指出的,对于我们许多人来说,自增整数是首选的数据库主键(即使在它不是最佳选择的情况下),我们希望我们的 Web 应用程序也能效仿。
Identity 2.0 核心类使用泛型类型参数
正如我们在关于自定义 ASP.NET Identity 2.0 用户和角色的文章中讨论的那样,该框架由泛型接口和基类的结构构成。在最低层,我们找到了接口,例如IUser<TKey>
和IRole<TKey>
。这些以及相关的接口和基类定义在Microsoft.AspNet.Identity.Core 库
.
将抽象级别提高一级,我们可以查看Microsoft.AspNet.Identity.EntityFramework
库,它使用定义在…Identity.Core
中的组件来构建应用程序中常用且特别是我们用于探索 Identity 2.0 的 Identity Samples 项目中常用的有用、即用型类。
该…Identity.EntityFramework
库为我们提供了一些泛型基类,以及每个类的默认具体实现。例如,…Identity.EntityFramework
为类提供了以下泛型基实现IdentityRole
:
IdentityRole 的泛型基
public class IdentityRole<TKey, TUserRole> : IRole<TKey>
where TUserRole : IdentityUserRole<TKey>
{
public TKey Id { get; set; }
public string Name { get; set; }
public ICollection<TUserRole> Users { get; set; }
public IdentityRole()
{
this.Users = new List<TUserRole>();
}
}
如我们所见,上面定义了IdentityRole
根据键的泛型类型参数和UserRole
,并且必须实现接口IRole<TKey>
。请注意,Identity 定义了一个IdentityRole
类,以及一个IdentityUserRole
类,两者都是使事情正常运行所必需的。稍后会详细介绍。
Identity 团队还提供了这个类的默认实现
IdentityRole 的默认实现,带有非泛型类型参数
public class IdentityRole : IdentityRole<string, IdentityUserRole>
{
public IdentityRole()
{
base.Id = Guid.NewGuid().ToString();
}
public IdentityRole(string roleName) : this()
{
base.Name = roleName;
}
}
请注意默认实现类是如何根据字符串
键和特定的实现定义的IdentityUserRole
?
这意味着我们只能将字符串作为键传递,实际上IdentityRole
模型将在我们的数据库中定义一个字符串类型的主键。它还意味着特定的、非泛型实现IdentityUserRole
将作为类型参数传递给基类。
如果我们借用上一篇文章中的内容,并查看 Identity 2.0 提供的默认类型定义,我们会发现以下内容(它不详尽,但这些是我们将要处理的)
带默认类型参数的默认 Identity 2.0 类签名
public class IdentityUserRole
: IdentityUserRole<string>
public class IdentityRole
: IdentityRole<string, IdentityUserRole>
public class IdentityUserClaim
: IdentityUserClaim<string>
public class IdentityUserLogin
: IdentityUserLogin<string>
public class IdentityUser
: IdentityUser<string, IdentityUserLogin,
IdentityUserRole, IdentityUserClaim>, IUser, IUser<string>
public class IdentityDbContext
: IdentityDbContext<IdentityUser, IdentityRole, string,
IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
public class UserStore<TUser>
: UserStore<TUser, IdentityRole, string, IdentityUserLogin,
IdentityUserRole, IdentityUserClaim>,
IUserStore<TUser>, IUserStore<TUser, string>, IDisposable
where TUser : IdentityUser
public class RoleStore<TRole>
: RoleStore<TRole, string, IdentityUserRole>, IQueryableRoleStore<TRole>,
IQueryableRoleStore<TRole, string>, IRoleStore<TRole, string>, IDisposable
where TRole : IdentityRole, new()
我们可以看到,从IdentityUserRole
开始,类型是用字符串键定义的,同样重要的是,它们是根据其他类型逐步定义的。这意味着如果我们想为所有模型(以及相应的数据库表)使用整数键而不是字符串键,我们需要基本上实现我们自己的上述堆栈版本。
使用 Identity 2.0 和 Identity 示例项目实现整数键
与之前的文章一样,我们将使用 Identity Samples 项目作为我们创建 Identity 2.0 MVC 应用程序的基础。Identity 团队主要(我假设)将 Identity Samples 项目作为一个演示平台,但实际上它包含了构建一个完整的 ASP.NET MVC 项目所需的一切(经过一些调整后),使用 Identity 2.0 框架。
我们在这里要研究的概念同样适用于你从头开始构建自己的基于 Identity 的应用程序。方法和手段可能会根据你的需求而有所不同,但总的来说,我们在这里看到的大部分内容都将适用,无论你是从 Identity Samples 项目作为基础开始,还是“自己动手”。
需要记住的重要一点是,Identity 框架提供的泛型基类型和接口允许很大的灵活性,但也引入了与泛型类型参数引入的依赖关系相关的复杂性。特别是,指定为每个模型的键的类型必须在堆栈中传播,否则编译器会报错。
入门 - 安装 Identity 示例项目
Identity 示例项目可在 Nuget 上获得。首先,创建一个空的 ASP.NET Web 项目(重要的是在这里使用“空”模板
,而不是 MVC,也不是 Webforms,是空的)。然后打开 Package Manager 控制台并输入
从 Package Manager 控制台安装 Identity 示例
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
这可能需要一两分钟才能运行。完成后,你将在 VS Solution Explorer 中看到一个基本的 ASP.NET MVC 项目。仔细查看 Identity 2.0 示例项目,并熟悉各个部分是什么以及它们在哪里。
重新设计基本身份模型
首先,我们需要重新设计 Identity Samples 项目中定义的基本模型类,并添加一些新的模型类。由于 Identity Samples 对实体模型使用基于字符串的键,因此作者在许多情况下可以依赖框架本身提供的默认类实现。在扩展时,它们从默认类扩展,这意味着基于字符串的键仍然内置于派生类中。
由于我们希望为所有模型使用整数键,因此我们需要为大多数模型提供自己的实现。
在许多情况下,这并没有听起来那么糟糕。例如,有少数模型类我们只需要根据泛型参数定义,然后基类实现就会完成其余的工作。
注意:
随着我们在这里修改/添加新类,Visual Studio 中的错误列表将像圣诞树一样亮起来,直到我们完成。暂时不要管它。如果我做得对,完成时应该没有错误。如果有,它们将帮助我们找到遗漏的地方。
在Models => IdentityModels.cs文件中,我们找到了 Identity Samples 应用程序使用的模型类。首先,我们将为IndentityUserLogin
, IdentityUserClaim
, andIdentityUserRole
添加我们自己的定义。Identity Samples 项目只依赖于这些类的默认框架实现,我们需要我们自己的基于整数的版本。将以下内容添加到IdentityModels.cs文件
基于整数的 UserLogin、UserClaim 和 UserRole 定义
public class ApplicationUserLogin : IdentityUserLogin<int> { }
public class ApplicationUserClaim : IdentityUserClaim<int> { }
public class ApplicationUserRole : IdentityUserRole<int> { }
现在,解决了这个问题,我们可以定义我们自己的实现IdentityRole
。示例项目也依赖于框架版本IdentityRole
,我们将再次提供我们自己的。然而,这次,它有更多内容
IdentityRole 的基于整数的定义
public class ApplicationRole : IdentityRole<int, ApplicationUserRole>, IRole<int>
{
public string Description { get; set; }
public ApplicationRole() { }
public ApplicationRole(string name)
: this()
{
this.Name = name;
}
public ApplicationRole(string name, string description)
: this(name)
{
this.Description = description;
}
}
请注意,上面我们已经定义了ApplicationRole
以整数键的形式,并且还以我们的自定义类ApplicationUserRole
的形式。这很重要,并且随着我们重新实现 Identity Samples 项目运行所需的 Identity 类,它将继续向上堆栈。
接下来,我们将修改现有的定义ApplicationUser
。目前,IdentitySamples.cs文件包含一个相当简单的定义ApplicationUser
它派生自默认的IdentityUser
框架提供的类,不需要类型参数,因为它们已在默认实现中提供。我们基本上需要重新定义ApplicationUser
从头开始。
IdentityModels.cs文件中的现有ApplicationUser
类如下所示
IdentityModels.cs 中现有的 ApplicationUser 类
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity>
GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
var userIdentity = await manager
.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
return userIdentity;
}
}
我们需要完全替换上述内容为以下内容:
ApplicationUser 的自定义实现
public class ApplicationUser
: IdentityUser<int, ApplicationUserLogin,
ApplicationUserRole, ApplicationUserClaim>, IUser<int>
{
public async Task<ClaimsIdentity>
GenerateUserIdentityAsync(UserManager<ApplicationUser, int> manager)
{
var userIdentity = await manager
.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
return userIdentity;
}
}
再次,我们没有从默认的 Identity 框架实现派生IdentityUser
,而是使用了泛型基,并提供了我们自己的自定义类型参数。再次,我们已将自定义ApplicationUser
定义为整数键和我们自己的自定义类型。
修改后的应用程序数据库上下文
在 IdentityModels.cs 文件中还有一个ApplicationDbContext
类。
现在我们已经构建了我们将要使用的基本模型,我们还需要重新定义ApplicationDbContext
根据这些新模型。与以前一样,Identity Samples 应用程序中使用的现有ApplicationDbContext
仅根据ApplicationUser
表示,(再次)依赖于框架提供的默认具体实现。
如果我们查看内部,我们会发现ApplicationDbContext<ApplicationUser>
实际上继承自IdentityDbContext<ApplicationUser>,
而它又派生自
IdentityDbContext<TUser, IdentityRole, string, IdentityUserLogin,
IdentityUserRole, IdentityUserClaim>
where TUser : Microsoft.AspNet.Identity.EntityFramework.IdentityUser
换句话说,我们再次拥有一个默认的具体实现,它根据我们迄今为止看到的其他默认框架类型定义,所有这些类型都进一步依赖于基于字符串的键。
为了定义一个DbContext
它将与我们的新自定义类型一起工作,我们需要用整数键和我们自己的自定义派生类型来表达我们的具体类。
将现有的ApplicationDbContext
代码替换为以下内容
修改后的 ApplicationDbContext
public class ApplicationDbContext
public class ApplicationDbContext
: IdentityDbContext
{
public ApplicationDbContext()
: base("DefaultConnection")
{
}
static ApplicationDbContext()
{
Database.SetInitializer(new ApplicationDbInitializer());
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
再次,我们现在已经表达了ApplicationDbContext
使用我们自己的自定义类型,所有这些类型都使用整数键而不是字符串。
自定义用户和角色存储
我敢打赌,如果你现在查看 Visual Studio 错误窗口,它很可能是一堆似乎是无休止的红色错误指示器。如前所述,目前没关系——忽略它。
Identity 框架定义了用于访问用户和角色信息的用户和角色存储的概念。与迄今为止的大多数其他事物一样,UserStore 和 RoleStore 的默认框架实现是根据我们迄今为止看到的其他默认类定义的——换句话说,它们不适用于我们的新自定义类。我们需要根据整数键和我们自己的自定义类来表达自定义用户存储和自定义角色存储。
将以下内容添加到 IdentityModels.cs 文件
添加自定义用户存储
public class ApplicationUserStore
: UserStore<ApplicationUser, ApplicationRole, int,
ApplicationUserLogin, ApplicationUserRole,
ApplicationUserClaim>, IUserStore<ApplicationUser, int>,
IDisposable
{
public ApplicationUserStore() : this(new IdentityDbContext())
{
base.DisposeContext = true;
}
public ApplicationUserStore(DbContext context)
: base(context)
{
}
}
public class ApplicationRoleStore
: RoleStore<ApplicationRole, int, ApplicationUserRole>,
IQueryableRoleStore<ApplicationRole, int>,
IRoleStore<ApplicationRole, int>, IDisposable
{
public ApplicationRoleStore()
: base(new IdentityDbContext())
{
base.DisposeContext = true;
}
public ApplicationRoleStore(DbContext context)
: base(context)
{
}
}
Identity Samples 项目包含一个名为 App_Start => IdentityConfig.cs 的文件。此文件中包含大量代码,这些代码基本上配置 Identity 系统以在你的应用程序中使用。我们对 IdentityModels.cs 文件所做的更改将在此处(以及基本上在整个应用程序中)导致问题,直到在客户端代码中解决它们。
在大多数情况下,我们将替换对默认 Identity 类的引用,改为使用我们的新自定义类,和/或调用允许传递自定义类型参数的方法重写。
在 IdentityConfig.cs 文件中,我们找到了一个ApplicationUserManager
类,其中包含我们的应用程序通常调用以管理用户和行为的代码。我们将用以下代码替换现有代码,该代码本质上将ApplicationUserManager
表示为整数键和我们的新自定义UserStore
。如果你仔细观察,我们已将 int 类型参数添加到许多方法调用中。
自定义 ApplicationUserManager 类
// *** PASS IN TYPE ARGUMENT TO BASE CLASS:
public class ApplicationUserManager : UserManager<ApplicationUser, int>
{
// *** ADD INT TYPE ARGUMENT TO CONSTRUCTOR CALL:
public ApplicationUserManager(IUserStore<ApplicationUser, int> store)
: base(store)
{
}
public static ApplicationUserManager Create(
IdentityFactoryOptions<ApplicationUserManager> options,
IOwinContext context)
{
// *** PASS CUSTOM APPLICATION USER STORE AS CONSTRUCTOR ARGUMENT:
var manager = new ApplicationUserManager(
new ApplicationUserStore(context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
manager.UserValidator = new UserValidator<ApplicationUser, int>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = true;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
manager.MaxFailedAccessAttemptsBeforeLockout = 5;
// Register two factor authentication providers.
// This application uses Phone and Emails as a step of receiving a
// code for verifying the user You can write your own provider and plug in here.
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
manager.RegisterTwoFactorProvider("PhoneCode",
new PhoneNumberTokenProvider<ApplicationUser, int>
{
MessageFormat = "Your security code is: {0}"
});
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
manager.RegisterTwoFactorProvider("EmailCode",
new EmailTokenProvider<ApplicationUser, int>
{
Subject = "SecurityCode",
BodyFormat = "Your security code is {0}"
});
manager.EmailService = new EmailService();
manager.SmsService = new SmsService();
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser, int>(
dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
那是一大堆代码。幸运的是,修改ApplicationRoleManager
类并不是什么大问题。我们本质上在做同样的事情——表达ApplicationRoleManager
以整数类型参数和我们的自定义类。
替换ApplicationRoleManager
代码替换为以下内容
自定义 ApplicationRoleManager 类
// PASS CUSTOM APPLICATION ROLE AND INT AS TYPE ARGUMENTS TO BASE:
public class ApplicationRoleManager : RoleManager<ApplicationRole, int>
{
// PASS CUSTOM APPLICATION ROLE AND INT AS TYPE ARGUMENTS TO CONSTRUCTOR:
public ApplicationRoleManager(IRoleStore<ApplicationRole, int> roleStore)
: base(roleStore)
{
}
// PASS CUSTOM APPLICATION ROLE AS TYPE ARGUMENT:
public static ApplicationRoleManager Create(
IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context)
{
return new ApplicationRoleManager(
new ApplicationRoleStore(context.Get<ApplicationDbContext>()));
}
}
该ApplicationDbInitializer
类是管理应用程序后端数据库创建和种子化的类。在这个类中,我们创建了一个基本的管理员角色用户,并设置了电子邮件和短信提供程序等其他项目。
我们唯一需要更改的是我们初始化一个实例的地方ApplicationRole
。在现有代码中,ApplicationDbInitializer
类实例化一个IdentityRole
实例,我们需要创建我们自己的ApplicationRole
实例。
用以下代码替换现有代码,或进行下面突出显示的更改
修改 ApplicationDbInitializer 类
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
{
protected override void Seed(ApplicationDbContext context)
{
InitializeIdentityForEF(context);
base.Seed(context);
}
//Create User=Admin@Admin.com with password=Admin@123456 in the Admin role
public static void InitializeIdentityForEF(ApplicationDbContext db) {
var userManager = HttpContext.Current.GetOwinContext()
.GetUserManager<ApplicationUserManager>();
var roleManager = HttpContext.Current.GetOwinContext()
.Get<ApplicationRoleManager>();
const string name = "admin@example.com";
const string password = "Admin@123456";
const string roleName = "Admin";
//Create Role Admin if it does not exist
var role = roleManager.FindByName(roleName);
if (role == null) {
// *** INITIALIZE WITH CUSTOM APPLICATION ROLE CLASS:
role = new ApplicationRole(roleName);
var roleresult = roleManager.Create(role);
}
var user = userManager.FindByName(name);
if (user == null) {
user = new ApplicationUser { UserName = name, Email = name };
var result = userManager.Create(user, password);
result = userManager.SetLockoutEnabled(user.Id, false);
}
// Add user admin to Role Admin if not already added
var rolesForUser = userManager.GetRoles(user.Id);
if (!rolesForUser.Contains(role.Name)) {
var result = userManager.AddToRole(user.Id, role.Name);
}
}
}
修复ApplicationSignInManager
甚至更简单。只需将类声明中的字符串
类型参数更改为int
:
修改 ApplicationSignInManager 类
// PASS INT AS TYPE ARGUMENT TO BASE INSTEAD OF STRING:
public class ApplicationSignInManager : SignInManager<ApplicationUser, int>
{
public ApplicationSignInManager(
ApplicationUserManager userManager, IAuthenticationManager authenticationManager) :
base(userManager, authenticationManager) { }
public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
{
return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
}
public static ApplicationSignInManager Create(
IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
{
return new ApplicationSignInManager(
context.GetUserManager<ApplicationUserManager>(), context.Authentication);
}
}
Cookie 身份验证配置
在 App_Start => Startup.Auth 文件中有一个分部类定义 Startup。在分部类中定义的单个方法调用中,有一个对app.UseCookieAuthentication()
的调用。现在我们的应用程序使用整数作为键而不是字符串,我们需要修改CookieAuthenticationProvider
的实例化方式。
对 app. 的现有调用。UseCookieAuthentication
(位于ConfigureAuth()
方法中间)需要修改。当代码调用OnVlidateIdentity
时,现有代码传递ApplicationUserManager
和ApplicationUser
作为类型参数。不明显的是,这是一个假定键的第三个字符串类型参数的重载(是的——我们又回到了字符串键的问题)。
我们需要更改此代码以调用另一个重载,该重载接受第三个类型参数,并传递一个int
参数。
现有代码如下所示
对 app.UseCookieAuthentication 的现有调用
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a
// password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator
.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user)
=> user.GenerateUserIdentityAsync(manager))
}
});
我们需要以几种不明显的方式修改此代码。首先,如上所述,我们需要添加第三个类型参数,指定TKey
是一个 int。
不太明显的是,我们还需要将第二个参数的名称从regenerateIdentity
toregenerateIdentityCallback
。相同的参数,但在我们使用的重载中名称不同。
另外不太明显的是第三个功能
我们需要作为getUserIdCallback
传递给调用。在这里,我们需要从存储为字符串的 Id 的声明中检索用户 id。我们需要将结果解析回一个int
.
将上面的现有代码替换为以下内容
修改后的 app.UseCookieAuthentication 调用
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a
// password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator
// ADD AN INT AS A THIRD TYPE ARGUMENT:
.OnValidateIdentity(
validateInterval: TimeSpan.FromMinutes(30),
// THE NAMED ARGUMENT IS DIFFERENT:
regenerateIdentityCallback: (manager, user)
=> user.GenerateUserIdentityAsync(manager),
// Need to add THIS line because we added the third type argument (int) above:
getUserIdCallback: (claim) => int.Parse(claim.GetUserId()))
}
});
有了这些,大部分 Identity 基础设施都已就位。现在我们需要更新应用程序中的一些内容。
更新 Admin 视图模型
Models => AdminViewModels.cs 文件包含RolesAdminViewModel
和UsersAdminViewModel
的类定义。在这两种情况下,我们都需要将 Id 属性的类型从字符串更改为 int
修改 Admin 视图模型
public class RoleViewModel
{
// Change the Id type from string to int:
public int Id { get; set; }
[Required(AllowEmptyStrings = false)]
[Display(Name = "RoleName")]
public string Name { get; set; }
}
public class EditUserViewModel
{
// Change the Id Type from string to int:
public int Id { get; set; }
[Required(AllowEmptyStrings = false)]
[Display(Name = "Email")]
[EmailAddress]
public string Email { get; set; }
public IEnumerable<SelectListItem> RolesList { get; set; }
}
更新控制器方法参数
许多控制器操作方法目前都期望一个字符串类型的 ID 参数。我们需要遍历控制器中的所有方法,并将 ID 参数的类型从字符串更改为 int。
在以下每个控制器中,我们需要将现有 ID 从字符串更改为 int,如所示的操作方法所示(我们只显示修改后的方法签名)
账户控制器
public async Task<ActionResult> ConfirmEmail(int userId, string code)
角色管理控制器
public async Task<ActionResult> Edit(int id)
public async Task<ActionResult> Details(int id)
public async Task<ActionResult> Delete(int id)
public async Task<ActionResult> DeleteConfirmed(int id, string deleteUser)
用户管理控制器
public async Task<ActionResult> Details(int id)
public async Task<ActionResult> Edit(int id)
public async Task<ActionResult> Delete(int id)
public async Task<ActionResult> DeleteConfirmed(int id)
更新 Roles Admin Controller 上的 Create 方法
无论我们何时创建 Role 的新实例,我们都需要确保我们使用的是新的ApplicationRole
而不是默认的IdentityRole
。具体来说,在Create()
方法中,在RolesAdminController
:
实例化一个新的 ApplicationRole 而不是 IdentityRole
[HttpPost]
public async Task<ActionResult> Create(RoleViewModel roleViewModel)
{
if (ModelState.IsValid)
{
// Use ApplicationRole, not IdentityRole:
var role = new ApplicationRole(roleViewModel.Name);
var roleresult = await RoleManager.CreateAsync(role);
if (!roleresult.Succeeded)
{
ModelState.AddModelError("", roleresult.Errors.First());
return View();
}
return RedirectToAction("Index");
}
return View();
}
如果我们现在查看错误列表,我们会发现大部分错误都与对User.Identity.GetUserId()
的调用有关。如果我们仔细查看这个方法,我们会发现默认版本的GetUserId()
再次返回一个字符串,并且存在一个接受类型参数的重载,该类型参数决定了返回类型。
遗憾的是,对GetUserId()
的调用在ManageController
中大量存在,并且在AccountController
中也有一些地方。我们需要更改所有调用以反映正确的类型参数,最有效的方法是进行老式的查找/替换。
幸运的是,你可以对两个ManageController
和AccountController
使用“查找/替换”来处理整个文档,从而一次性完成所有操作。按下 Ctrl + H,在“查找”框中输入以下内容
查找所有实例
Identity.GetUserId()
替换为
Identity.GetUserId<int>()
如果我们做得正确,错误列表中的大多数显着红色错误现在应该已经消失。但是,还有一些遗留问题。在这些情况下,我们需要反直觉地将 int Id 转换回字符串。
在需要时返回字符串
有一些方法调用了GetUserId()
,但无论 Id 所代表的类型(在我们的例子中,现在是int
)如何,都希望将 Id 的字符串表示形式作为参数传递。所有这些方法都可以在ManageController
上找到,并且在每种情况下,我们只需添加对.ToString()
.
的调用首先,在
Index()ManageController
方法中,在AuthenticationManager.TwoFactorBrowserRemembered()
中找到一个调用。在对ToString()
的调用之后,添加对GetUserId()
:
的调用
public async Task<ActionResult> Index(ManageMessageId? message)
{
ViewBag.StatusMessage =
message == ManageMessageId.ChangePasswordSuccess ?
"Your password has been changed."
: message == ManageMessageId.SetPasswordSuccess ?
"Your password has been set."
: message == ManageMessageId.SetTwoFactorSuccess ?
"Your two factor provider has been set."
: message == ManageMessageId.Error ?
"An error has occurred."
: message == ManageMessageId.AddPhoneSuccess ?
"The phone number was added."
: message == ManageMessageId.RemovePhoneSuccess ?
"Your phone number was removed."
: "";
var model = new IndexViewModel
{
HasPassword = HasPassword(),
PhoneNumber = await UserManager.GetPhoneNumberAsync(User.Identity.GetUserId<int>()),
TwoFactor = await UserManager.GetTwoFactorEnabledAsync(User.Identity.GetUserId<int>()),
Logins = await UserManager.GetLoginsAsync(User.Identity.GetUserId<int>()),
// *** Add .ToString() to call to GetUserId():
BrowserRemembered = await AuthenticationManager
.TwoFactorBrowserRememberedAsync(User.Identity.GetUserId<int>().ToString())
};
return View(model);
}
添加到 TwoFactorBrowserRemembered 的 ToString() 调用同样,对
RememberBrowserManageController
:
方法也执行相同的操作,同样在
[HttpPost]
public ActionResult RememberBrowser()
{
var rememberBrowserIdentity = AuthenticationManager
.CreateTwoFactorRememberBrowserIdentity(
// *** Add .ToString() to call to GetUserId():
User.Identity.GetUserId<int>().ToString());
AuthenticationManager.SignIn(
new AuthenticationProperties { IsPersistent = true },
rememberBrowserIdentity);
return RedirectToAction("Index", "Manage");
}
将 ToString() 调用添加到 RememberBrowser 方法最后,对
和LinkLogin()
方法
LinkLoginCallback()
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LinkLogin(string provider)
{
return new AccountController
.ChallengeResult(provider, Url.Action("LinkLoginCallback", "Manage"),
// *** Add .ToString() to call to GetUserId():
User.Identity.GetUserId<int>().ToString());
}
将 ToString() 调用添加到 LinkLogin()
public async Task<ActionResult> LinkLoginCallback()
{
var loginInfo = await AuthenticationManager
.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId<int>().ToString());
if (loginInfo == null)
{
return RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error });
}
var result = await UserManager
// *** Add .ToString() to call to GetUserId():
.AddLoginAsync(User.Identity.GetUserId<int>().ToString(), loginInfo.Login);
return result.Succeeded ? RedirectToAction("ManageLogins")
: RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error });
}
将 ToString() 调用添加到 LinkLoginCallback()
至此,我们已经解决了大部分严重问题,我们基本上已经将一个使用所有字符串键构建的项目转换为了使用整数。整数类型也将作为自增整数主键传播到数据库后端。
但还有一些事情需要清理。
修复整数类型的空检查
在主要身份控制器中散布着大量针对方法调用中作为参数接收的 Id 值的空检查。如果你重建项目,Visual Studio 中的错误列表窗口现在应该包含大量关于此问题的黄色“警告”项。你可以按照你偏好的方式处理这个问题,但对我来说,我更喜欢检查正整数值。我们将以
Details()方法为例
UserAdminController
为例,你可以从那里开始。你可以按照你偏好的方式处理这个问题,但对我来说,我更喜欢检查正整数值。我们将以
方法中的现有代码如下所示
UserAdminController 中现有的 Details() 方法
public async Task<ActionResult> Details(int id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var user = await UserManager.FindByIdAsync(id);
ViewBag.RoleNames = await UserManager.GetRolesAsync(user.Id);
return View(user);
}
在上面,我们可以看到,之前,代码检查了(以前的)字符串类型 Id 参数的空值。现在我们正在接收一个int
,空检查是无意义的。相反,我们希望检查一个正整数值。如果检查为真,那么我们希望相应地进行处理。否则,我们希望返回BadRequest
结果。
换句话说,我们需要反转方法逻辑。以前,如果条件评估为真,我们希望返回错误代码。现在,如果结果为真,我们希望继续,只有当条件为假时才返回错误结果。所以我们将交换我们的逻辑。
用以下代码替换现有代码
带有反转条件逻辑的修改后的 Details() 方法
public async Task<ActionResult> Details(int id)
{
if (id > 0)
{
// Process normally:
var user = await UserManager.FindByIdAsync(id);
ViewBag.RoleNames = await UserManager.GetRolesAsync(user.Id);
return View(user);
}
// Return Error:
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
我们可以对方法为例
, RolesAdminController
, andAccountController
中的其他情况做类似的事情。仔细思考逻辑,一切都会好起来的。
更新角色管理视图
一些视图模板目前使用默认的 IdentityRole 模型,而不是我们新的自定义ApplicationRole
。我们需要更新 Views => RolesAdmin 中的视图以反映我们的新自定义模型。
Create.cshtml 和 Edit.cshtml 视图都依赖于RoleViewModel
,这很好。但是,Index.cshtml、Details.cshtml 和 Delete.cshtml 视图目前都引用IdentityRole
。按如下方式更新所有三个
Index.cshtml 视图目前期望一个IEnumerable<IdentityRole>
。我们需要将其更改为期望一个IEnumerable<ApplicationRole
>。请注意,我们还需要包含项目模型命名空间
更新 RolesAdmin Index.cshtml 视图
@model IEnumerable<IdentitySample.Models.ApplicationRole>
// ... All the view code ...
我们只需要更改第一行,所以我省略了视图代码的其余部分。
同样,我们需要更新 Details.cshtml 和 Delete.cshtml 视图以期望ApplicationRole
而不是IdentityRole
。将每个视图的第一行更改为与以下内容匹配
更新 Details.cshtml 和 Delete.cshtml 视图
@model IdentitySample.Models.ApplicationRole
// ... All the view code ...
显然,如果你的默认项目命名空间不是IdentitySamples
,请根据需要进行更改。
现在扩展变得容易了
现在我们基本上已经用我们自己的派生类型重新实现了大部分 Identity 对象模型,可以很容易地向 ApplicationUser 和/或 ApplicationRole 模型添加自定义属性。我们所有的自定义类型都已经根据相互关联的泛型类型参数相互依赖,因此我们可以自由地添加我们希望添加的属性,然后相应地更新我们的控制器、视图模型和视图。
为此,请查看之前关于扩展用户和角色的文章,但请注意所有类型结构的东西都已经完成。查看该文章只是为了了解如何更新控制器、视图和视图模型。
关于安全性的说明
基本的 Identity Samples 应用程序是构建你自己的 Identity 2.0 应用程序的一个很好的起点。但是,请注意,作为演示,其中包含一些不应出现在生产代码中的内容。例如,数据库初始化目前包含硬编码的管理员用户凭据。
此外,电子邮件确认和双因素身份验证功能目前通过在每个页面上包含绕过该过程的链接来规避实际的确认和双因素过程。
在部署基于 Identity Samples 项目的实际应用程序之前,应解决上述问题。
总结
我们已经详尽地研究了如何修改 Identity Samples 应用程序以使用整数键而不是字符串。在此过程中,我们(希望)对基于 Identity 2.0 的应用程序中的底层结构有了更深入的理解。还有很多东西要学习,但这只是一个好的开始。