ASP.NET MVC 和 Identity 2.0:了解基础知识
2014 年 3 月 20 日,ASP.NET 团队发布了新 Identity 框架的 RTM 版本 2.0。新版本带来了一些期待已久的新功能,并极大地扩展了所有类型 ASP.NET 应用程序可用的安全性和授权功能。ASP.NE
2014 年 3 月 20 日,ASP.NET 团队发布了新 Identity 框架的 RTM 版本 2.0。新版本带来了一些期待已久的新功能,并极大地扩展了所有类型 ASP.NET 应用程序可用的安全性和授权功能。
ASP.NET Identity 框架最初于 2013 年推出,作为 ASP.NET Membership 系统的后续。ASP.NET Membership 曾是 MVC 应用程序多年的标配,但已开始显露老态。最初,ASP.NET Identity 为管理使用 ASP.NET 构建的面向公众的 Web 应用程序的安全性和授权提供了一个有用的、尽管有些简化的 API。Identity 框架引入了现代功能,例如社交网络登录集成,以及易于扩展的用户模型定义。
图片由 Ryan Somma 拍摄 | 部分权利保留
新 RTM 版本除其他功能外,还引入了以下功能:
- 扩展的用户帐户定义,包括电子邮件和联系信息
- 通过电子邮件或短信进行双因素身份验证,功能上类似于 Google、Microsoft 等公司使用的
- 通过电子邮件进行帐户确认
- 用户和角色的管理
- 帐户锁定,以响应无效的登录尝试
- 安全令牌提供程序,用于在安全设置更改时重新生成用户的安全令牌
- 改进了对社交登录的支持
- 轻松集成基于声明的授权
Identity 2.0 是对去年推出的原始版本进行的重大修订。随着众多新功能的出现,也带来了一些额外的复杂性。如果您和我一样,刚刚才熟悉了 Identity 框架的第一版,那么请做好准备。虽然使用 2.0 版本您不必从头开始,但仍有很多内容需要学习。
在本文中,我们将进行一次概览,熟悉系统的主要组件,并总体上了解新功能及其在整体中的位置。我们暂时不会深入细节。可以将其视为一次熟悉之旅。
如果您正在寻找更详细的操作指南,我将在接下来的几周内添加更多文章,探讨具体的实现细节:
ASP.NET 4.5 项目模板(我们在此处将要检查的)集成了 OWIN,特别是 Katana 实现。有关 OWIN、Katana 和中间件管道的更多信息,请参阅 ASP.NET:理解 OWIN、Katana 和中间件管道。
如果您计划将 Identity 2.0 与 Web API 结合使用,情况会略有不同。您可能需要查看以下文章:
- ASP.NET Identity 2.0:Identity 2.0 和 Web API 2.2 入门
- ASP.NET Web API 和 Identity 2.0 - 自定义 Identity 模型和实现基于角色的授权
虽然我们会看相当多的代码,但目前还不必理解所有代码的细节——只需熟悉一般概念、主要组件的位置以及事物的结构即可。
- Identity 2.0 配置 - 不再那么简单
- Identity 2.0 中的新 ApplicationUser 类
- Identity 2.0 配置组件和助手
- ApplicationUserManager 和 ApplicationRoleManager
- 用于帐户验证和双因素身份验证的电子邮件服务和短信服务
- 登录助手 - 基本的 Identity 2.0 登录 API
- 登录:与 Identity 1.0 对比
- 核心所在 - ApplicationDbContext
- 其他资源和感兴趣的项目
Identity 2.0 引入了破坏性更改
Identity 2.0 并不能平滑地集成到使用 1.0 版本编写的应用程序中。额外的功能似乎需要对体系结构以及应用程序内部使用 Identity API 的方式进行重大更改。将现有的 ASP.NET 应用程序从 Identity 1.0 升级到 2.0 版本需要编写一些新代码,这超出了本文的范围。但请注意,从 Identity 1.0 迁移到 2.0 版本并非简单的“即插即用”操作。
入门 - 从 Nuget 获取示例
截至本文撰写之时,尚无直接可用的 ASP.NET MVC 项目模板使用 Identity 2.0。为了试用 Identity,您需要将示例项目库导入到一个空的 ASP.NET MVC 项目中。首先,创建一个新的 ASP.NET 项目,并在模板选项对话框中选择“空项目”模板
选择“空 ASP.NET 项目”模板
创建新空项目后,您可以通过在包管理器控制台中输入以下命令,从 Nuget 获取 Identity 2.0 示例项目:
从 Nuget 安装示例项目
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
Nuget 完成后,您应该会在解决方案资源管理器中看到一个与标准 MVC 项目非常相似的文件夹结构。Nuget 基本上添加了构成完整 ASP.NET MVC 项目所需的一切,包括模型、视图、控制器以及此基本应用程序运行所需的各种组件。
虽然乍一看项目组件看起来相当相似,但仔细观察会发现一些重大的变化和额外的复杂性。
Identity 2.0 配置 - 不再那么简单
在我看来,原始 Identity 框架的一个优点同时也是其主要缺点(软件中真是奇怪,不是吗?)。Identity 1.0 版本的简洁性使其易于使用,并且相对直观易懂。另一方面,极度的简洁性也意味着“开箱即用”的功能集有限,对某些人来说,可能不足。
为了有个大致了解,我们将快速查看应用程序启动时的一些配置,并与使用 Identity 1.0 的应用程序中的相应代码进行比较。
在这两种项目风格中,我们在项目根目录下都会找到一个名为 Startup.cs 的类文件。在此文件中定义了一个名为 Startup
的类,并调用了 ConfigureAuth()
方法。我们在该文件中任何地方都看不到一个实际名为 ConfigureAuth()
的方法。这是因为 Startup
类其余的代码定义在一个位于 App_Start 文件夹中的部分类中。该代码文件名为 Startup.Auth.cs,但如果我们打开它,会发现一个标准的局部类定义,其中包含 ConfigureAuth()
方法。在使用了 Identity 框架原始 1.0 版本的项目中,ConfigureAuth()
的标准代码如下所示:
使用 Identity 1.0 的标准 ConfigureAuth() 方法
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to
// store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
// Use a cookie to temporarily store information about a
// user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Uncomment the following lines to enable logging
// in with third party login providers
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");
//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");
//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");
//app.UseGoogleAuthentication();
}
}
在上面,我们看到了一些用于配置 cookie 的样板代码,以及一些被注释掉的代码,这些代码可以被取消注释,然后调用以启用来自各种社交媒体提供商的第三方登录。
相比之下,当我们查看使用 Identity 2.0 的项目中的 ConfigureAuth()
方法时,会发现添加了一些代码
使用 Identity 2.0 的项目中的 ConfigureAuth() 方法
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context, user manager and role
// manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
// Enable the application to use a cookie to store information for the
// signed in user and to use a cookie to temporarily store information
// about a user logging in with a third party login provider
// Configure the sign in cookie
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))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when
// they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(
DefaultAuthenticationTypes.TwoFactorCookie,
TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such
// as phone or email. Once you check this option, your second step of
// verification during the login process will be remembered on the device where
// you logged in from. This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(
DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
// Uncomment the following lines to enable logging in
// with third party login providers
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");
//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");
//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");
//app.UseGoogleAuthentication();
}
}
上面,我们首先注意到的是几次调用 app.CreatePerOwinContext
,其中我们注册了用于创建指定类型实例的回调。创建的类型实例随后可以使用 context.Get()
方法进行访问。
这告诉我们,至少对于 Identity 2.0 团队提供的示例项目而言,Owin 现在是应用程序的一部分,并且 Identity 2.0 依赖它来提供功能。我不清楚 Owin 是否是 Identity 2.0 工作所必需的,但就我们的示例项目而言,它是必需的。
更新:Microsoft 团队的 Rick Anderson 在我对原始博客文章的评论中提到:
“OWIN 并不是 Identity 2.0 所必需的,但我们特别使用 OWIN 作为一种简单的依赖注入(CreatePerContext),并且我们也使用 owin 生成 cookie/执行身份验证作为应用程序的一部分。您可以独立使用 UserManager/RoleManager 而无需 owin(例如,如果您仍想将 FormsAuth 与 identity 结合使用,也可以)”
有关更多信息,请参阅 ASP.NET:理解 OWIN、Katana 和中间件管道。
我们还可以看到 ConfigureAuth
方法体中还有其他一些新调用,用于设置双因素身份验证,以及一些在前一个版本中不存在的附加 cookie 身份验证配置代码。
就我们而言,可以假设大部分这些东西都已针对我们的基本使用进行了最佳配置,并且除了添加社交媒体登录(本文不涉及)之外,我们可以保持此代码不变。但请记住,您的应用程序中的许多 Identity 行为都是在运行时在此处设置的,具体取决于 App_Start 文件夹中另一个文件 IdentityConfig.cs 中定义的配置组件和助手。
在查看 IdentityConfig.cs 文件之前,先了解 Models 文件夹中定义的 ApplicationUser
类将有助于我们理解那里发生的事情。
Identity 2.0 中的新 ApplicationUser 类
如果您使用过前一个版本的 Identity 框架构建过应用程序,您可能会发现核心 IdentityUser
类相当有限。以前,Identity 使用一个非常简单的 IdentityUser
实现,它代表一个非常简化的用户配置文件。
Identity 1.0 版本中的原始 IdentityUser 类
public class IdentityUser : IUser
{
public IdentityUser();
public IdentityUser(string userName);
public virtual string Id { get; set; }
public virtual string UserName { get; set; }
public virtual ICollection<IdentityUserRole> Roles { get; }
public virtual ICollection<IdentityUserClaim> Claims { get; }
public virtual ICollection<IdentityUserLogin> Logins { get; }
public virtual string PasswordHash { get; set; }
public virtual string SecurityStamp { get; set; }
}
在上面的属性中,只有前三个 Id
、UserName
和 Roles
从我们应用程序的业务角度来看比较有用。其他项目主要由安全逻辑使用,虽然重要,但无助于我们维护有关用户的有用信息。
在之前的文章中,我们探讨了如何扩展 Identity 帐户并实现基于角色的身份验证(在 Identity 1.0 下)以添加更多有用数据,例如用户电子邮件地址和/或其他应用程序可能需要的信息。
随着 Identity 2.0 的问世,创建此类变通方法的需求有所减少。虽然仍然可以以类似的方式扩展 Identity 2.0 的 ApplicationUser
类,但 Identity 团队已经为我们处理了一些更常见的用例。
我们发现,示例项目已经包含了一个子类 ApplicationUser
,它继承自一个更复杂的默认 IdentityUser
实现。
我们在 Models 文件夹中,在一个名为 IdentityModels.cs 的文件中找到了 ApplicationUser
的定义。我们可以看到类定义本身非常简单:
Identity 2.0 中的 ApplicationUser 类
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one
// defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity =
await manager.CreateIdentityAsync(this,
DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
我们在此处看到 ApplicationUser
如前所述是 IdentityUser
的子类。但是,如果我们查找 IdentityUser
的定义(使用 VS 的“转到定义”上下文菜单项),我们会发现,在 Identity 2.0 框架下定义的 IdentityUser
本身是 IdentityUser<TKey, TLogin, TRole, TClaim>
的子类。当我们查看 THAT 的定义时,会发现它与 1.0 版本实现有显著不同:
Identity 2.0 中的 IdentityUser 实现
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey>
where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey>
where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey>
{
public IdentityUser();
// Used to record failures for the purposes of lockout
public virtual int AccessFailedCount { get; set; }
// Navigation property for user claims
public virtual ICollection<TClaim> Claims { get; }
// Email
public virtual string Email { get; set; }
// True if the email is confirmed, default is false
public virtual bool EmailConfirmed { get; set; }
// User ID (Primary Key)
public virtual TKey Id { get; set; }
// Is lockout enabled for this user
public virtual bool LockoutEnabled { get; set; }
// DateTime in UTC when lockout ends, any
// time in the past is considered not locked out.
public virtual DateTime? LockoutEndDateUtc { get; set; }
// Navigation property for user logins
public virtual ICollection<TLogin> Logins { get; }
// The salted/hashed form of the user password
public virtual string PasswordHash { get; set; }
// PhoneNumber for the user
public virtual string PhoneNumber { get; set; }
// True if the phone number is confirmed, default is false
public virtual bool PhoneNumberConfirmed { get; set; }
// Navigation property for user roles
public virtual ICollection<TRole> Roles { get; }
// A random value that should change whenever a users
// credentials have changed (password changed, login removed)
public virtual string SecurityStamp { get; set; }
// Is two factor enabled for the user
public virtual bool TwoFactorEnabled { get; set; }
// User name
public virtual string UserName { get; set; }
}
请注意,上面有很多属性再次与授权和安全相关,而不是与我们用户数据的业务需求相关。但是,Email
和 PhoneNumber
字段确实在很大程度上减少了对 ApplicationUser
类进行额外自定义的需求。
但是,类声明中的泛型类型参数是什么意思?
新版本的 IdentityUser
实现泛型类型参数,以提供额外的灵活性。例如,回想一下在 Identity 1.0 版本中,Id
属性是一个字符串。在这里,泛型类型参数 TKey
允许我们指定 Id
字段的类型。我们可以看到上面的 Id
属性声明返回由 TKey
指定的类型。
Id 属性声明
public virtual TKey Id { get; set; }
同样特别值得注意的是 Roles 属性,定义如下:
Roles 属性
public virtual ICollection<TRole> Roles { get; }
我们可以看到 TRole
类型在编译时是开放的,实际上是在 IdentityUser
类的泛型声明中指定的。如果我们查看该声明中的类型约束,会发现 TRole
被约束为 IdentityUserRole<TKey>
类型,这与 1.0 版本实现并没有太大区别。不同之处在于,并且代表了一个破坏性更改,是 IdentityUserRole
本身 的定义。
以前,在 Identity 框架的 1.0 版本中,IdentityUserRole
定义如下:
public class IdentityUserRole
{
public IdentityUserRole();
public virtual IdentityRole Role { get; set; }
public virtual string RoleId { get; set; }
public virtual IdentityUser User { get; set; }
public virtual string UserId { get; set; }
}
与 Identity 2.0 实现进行比较
public class IdentityUserRole<TKey>
{
public IdentityUserRole();
public virtual TKey RoleId { get; set; }
public virtual TKey UserId { get; set; }
}
看到了吗?前者包含对 IdentityRole
对象和 IdentityUser
对象的引用。2.0 版本实现仅包含 Id
值。如果您在前一个版本中进行了任何自定义,例如我们在实现基于组的权限中讨论的那样,这将破坏现有功能。
我们将在后续文章中更详细地研究新扩展的 IdentityUser
类带来的新灵活性。目前,请认识到,虽然基本用户类定义变得更加复杂,但它也变得更加灵活。
由于 ApplicationUser
是 IdentityUser
的子类,因此以上所有属性都可用于 ApplicationUser
,它是示例应用程序使用的基本实现。
现在我们已经快速了解了新的 ApplicationUser
实现,我们将要查看的配置组件和助手将更有意义。
Identity 2.0 配置组件和助手
虽然 Startup
类的 ConfigAuth()
方法是 Identity 在启动期间进行运行时配置的地方,但我们实际上使用 IdentityConfig.cs 文件中定义的组件来配置 Identity 2.0 功能在我们的应用程序中的行为方式。
如果我们检查 IdentityConfig.cs 文件的内容,会发现其中定义了许多独立的类。我们可以将每个类拆分成自己的代码文件,但目前,我们将单独检查每个类,尽管它们都位于项目中的同一文件位置。并非所有这些类都包含在 ApplicationName.Models
命名空间中。
ApplicationUserManager 和 ApplicationRoleManager
我们在 IdentityConfig.cs 文件中首先遇到的是两个助手类:ApplicationUserManager
和 ApplicationRoleManager
。请做好准备——大量包含泛型类型的代码即将来临!
Identity 2.0 Application User Manager 类
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store) : base(store)
{
}
public static ApplicationUserManager Create(
IdentityFactoryOptions<ApplicationUserManager> options,
IOwinContext context)
{
var manager = new ApplicationUserManager(
new UserStore<ApplicationUser>(
context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
manager.UserValidator =
new UserValidator<ApplicationUser>(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.
manager.RegisterTwoFactorProvider("PhoneCode",
new PhoneNumberTokenProvider<ApplicationUser>
{
MessageFormat = "Your security code is: {0}"
});
manager.RegisterTwoFactorProvider("EmailCode",
new EmailTokenProvider<ApplicationUser>
{
Subject = "SecurityCode",
BodyFormat = "Your security code is {0}"
});
manager.EmailService = new EmailService();
manager.SmsService = new SmsService();
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(
dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
public virtual async Task<IdentityResult> AddUserToRolesAsync(
string userId, IList<string> roles)
{
var userRoleStore = (IUserRoleStore<ApplicationUser, string>)Store;
var user = await FindByIdAsync(userId).ConfigureAwait(false);
if (user == null)
{
throw new InvalidOperationException("Invalid user Id");
}
var userRoles = await userRoleStore
.GetRolesAsync(user)
.ConfigureAwait(false);
// Add user to each role using UserRoleStore
foreach (var role in roles.Where(role => !userRoles.Contains(role)))
{
await userRoleStore.AddToRoleAsync(user, role).ConfigureAwait(false);
}
// Call update once when all roles are added
return await UpdateAsync(user).ConfigureAwait(false);
}
public virtual async Task<IdentityResult> RemoveUserFromRolesAsync(
string userId, IList<string> roles)
{
var userRoleStore = (IUserRoleStore<ApplicationUser, string>) Store;
var user = await FindByIdAsync(userId).ConfigureAwait(false);
if (user == null)
{
throw new InvalidOperationException("Invalid user Id");
}
var userRoles = await userRoleStore
.GetRolesAsync(user)
.ConfigureAwait(false);
// Remove user to each role using UserRoleStore
foreach (var role in roles.Where(userRoles.Contains))
{
await userRoleStore
.RemoveFromRoleAsync(user, role)
.ConfigureAwait(false);
}
// Call update once when all roles are removed
return await UpdateAsync(user).ConfigureAwait(false);
}
}
对于看起来很大的代码块,ApplicationUserManager
实际上只提供了几个非常重要的功能——添加新用户、将用户添加到角色以及从角色中删除用户。但是,ApplicationUserManager 继承自 UserManager<ApplicationUser>
类,因此 UserManager
提供的所有功能也都可用于 ApplicationUserManager
。除此之外,还有一个静态的 Create()
方法定义,它返回 ApplicationUserManager
本身的一个实例。在这个方法中,您的许多用户配置设置和默认值都已设置好。
在 Create()
方法中特别值得注意的是调用 context.Get<ApplicationDBContext>()
。还记得我们之前在查看 ConfigAuth()
方法时,那些调用 CreatePerOwinContext
并传递了一个回调方法吗?调用 context.Get<ApplicationDbContext>()
执行了那个回调,在这种情况下,就是静态方法 ApplicationDbContext.Create()
。我们很快就会看到更多。
如果您仔细观察,会发现用户授权、身份验证和管理设置以及默认值是在 Create()
方法中设置的,然后再将新的 ApplicationUserManager
实例返回给调用者。此外,这也是设置双因素身份验证服务的地方。我们可以看到大多数设置都相当直观。但是,这两个服务值得仔细研究。我们稍后会回到这个问题。首先,快速看一下 ApplicationRoleManager
类。
Application Role Manager 类
public class ApplicationRoleManager : RoleManager<IdentityRole>
{
public ApplicationRoleManager(IRoleStore<IdentityRole,string> roleStore)
: base(roleStore)
{
}
public static ApplicationRoleManager Create(
IdentityFactoryOptions<ApplicationRoleManager> options,
IOwinContext context)
{
var manager = new ApplicationRoleManager(
new RoleStore<IdentityRole>(
context.Get<ApplicationDbContext>()));
return manager;
}
}
与 ApplicationUserManager
一样,我们可以看到 ApplicationRoleManager
继承自 RoleManager<IdentityRole>
,因此也带来了该类提供的所有功能。再次,我们看到一个静态的 Create()
方法返回该类本身的实例。
用于帐户验证和双因素身份验证的电子邮件服务和短信服务
同样在 IdentityConfig.cs 文件中,有两个服务类:EmailService
和 SmsService
。开箱即用,这两个类基本上是空的包装器,提供了一个抽象,您可以在其中实现双因素身份验证和帐户验证所需的电子邮件和/或短信服务。
ASP.NET Identity Email Service 类
public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your email service here to send an email.
return Task.FromResult(0);
}
}
ASP.NET Identity SmsService 类
public class SmsService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your sms service here to send a text message.
return Task.FromResult(0);
}
}
请注意,这两个类都实现了一个共同的接口 IIdentityMessageService
。还记得 ApplicationUserManager.Create()
方法中的以下几行吗?
在 ApplicationUserManager Create 方法中设置电子邮件服务和短信服务
// 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.
manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser>
{
MessageFormat = "Your security code is: {0}"
});
manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser>
{
Subject = "SecurityCode",
BodyFormat = "Your security code is {0}"
});
manager.EmailService = new EmailService();
manager.SmsService = new SmsService();
我们可以看到,在 Create()
方法的过程中,初始化了 EmailService
和 SmsService
的新实例,并通过 ApplicationUserManager
实例上的相应属性引用它们。
登录助手 - 基本的 Identity 2.0 登录 API
在创建 Identity 示例项目时,Identity 团队添加了一个方便的助手类,该类也位于 IdentityConfig.cs 文件中,它将常用的登录和身份验证调用封装在一个高效且易于使用的 API 中。通过查看 Controllers 文件夹中的 AccountController
,我们可以看到这些方法是如何被使用的。但首先,让我们看一下 SignInHelper
类本身。
与前面的示例一样,我们不会在这里深入探讨细节,除了熟悉类的基本结构、可用的方法,并大致了解我们如何从应用程序内部使用 SignInHelper
中的方法。
登录助手类
public class SignInHelper
{
public SignInHelper(ApplicationUserManager userManager, IAuthenticationManager authManager)
{
UserManager = userManager;
AuthenticationManager = authManager;
}
public ApplicationUserManager UserManager { get; private set; }
public IAuthenticationManager AuthenticationManager { get; private set; }
public async Task SignInAsync(ApplicationUser user, bool isPersistent, bool rememberBrowser)
{
// Clear any partial cookies from external or two factor partial sign ins
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie,
DefaultAuthenticationTypes.TwoFactorCookie);
var userIdentity = await user.GenerateUserIdentityAsync(UserManager);
if (rememberBrowser)
{
var rememberBrowserIdentity =
AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(user.Id);
AuthenticationManager.SignIn(
new AuthenticationProperties { IsPersistent = isPersistent },
userIdentity,
rememberBrowserIdentity);
}
else
{
AuthenticationManager.SignIn(
new AuthenticationProperties { IsPersistent = isPersistent },
userIdentity);
}
}
public async Task<bool> SendTwoFactorCode(string provider)
{
var userId = await GetVerifiedUserIdAsync();
if (userId == null)
{
return false;
}
var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider);
// See IdentityConfig.cs to plug in Email/SMS services to actually send the code
await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token);
return true;
}
public async Task<string> GetVerifiedUserIdAsync()
{
var result = await AuthenticationManager.AuthenticateAsync(
DefaultAuthenticationTypes.TwoFactorCookie);
if (result != null && result.Identity != null
&& !String.IsNullOrEmpty(result.Identity.GetUserId()))
{
return result.Identity.GetUserId();
}
return null;
}
public async Task<bool> HasBeenVerified()
{
return await GetVerifiedUserIdAsync() != null;
}
public async Task<SignInStatus> TwoFactorSignIn(
string provider,
string code,
bool isPersistent,
bool rememberBrowser)
{
var userId = await GetVerifiedUserIdAsync();
if (userId == null)
{
return SignInStatus.Failure;
}
var user = await UserManager.FindByIdAsync(userId);
if (user == null)
{
return SignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code))
{
// When token is verified correctly, clear the access failed
// count used for lockout
await UserManager.ResetAccessFailedCountAsync(user.Id);
await SignInAsync(user, isPersistent, rememberBrowser);
return SignInStatus.Success;
}
// If the token is incorrect, record the failure which
// also may cause the user to be locked out
await UserManager.AccessFailedAsync(user.Id);
return SignInStatus.Failure;
}
public async Task<SignInStatus> ExternalSignIn(
ExternalLoginInfo loginInfo,
bool isPersistent)
{
var user = await UserManager.FindAsync(loginInfo.Login);
if (user == null)
{
return SignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
return await SignInOrTwoFactor(user, isPersistent);
}
private async Task<SignInStatus> SignInOrTwoFactor(ApplicationUser user, bool isPersistent)
{
if (await UserManager.GetTwoFactorEnabledAsync(user.Id) &&
!await AuthenticationManager.TwoFactorBrowserRememberedAsync(user.Id))
{
var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
AuthenticationManager.SignIn(identity);
return SignInStatus.RequiresTwoFactorAuthentication;
}
await SignInAsync(user, isPersistent, false);
return SignInStatus.Success;
}
public async Task<SignInStatus> PasswordSignIn(
string userName,
string password,
bool isPersistent,
bool shouldLockout)
{
var user = await UserManager.FindByNameAsync(userName);
if (user == null)
{
return SignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
if (await UserManager.CheckPasswordAsync(user, password))
{
return await SignInOrTwoFactor(user, isPersistent);
}
if (shouldLockout)
{
// If lockout is requested, increment access failed
// count which might lock out the user
await UserManager.AccessFailedAsync(user.Id);
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
}
return SignInStatus.Failure;
}
}
要研究的代码量很大,正如我所说,我们现在不会仔细研究。我们主要在这里是为了了解情况,熟悉环境。我们可以看到这个类中的方法似乎都与登录和授权职责有关。
SignInHelper
类中可用方法代表了 Identity 2.0 中引入的一些新功能。我们当然看到了一个熟悉的 SignInAsync()
方法,然后看到了大量与双因素授权和外部登录相关的新方法。此外,为了实现登录,似乎比以前要复杂得多。
我们可以查看示例项目中的 AccountController
上的 Login 方法,以了解 Identity 2.0 中如何处理身份验证。
使用 Identity 2.0 的 Account Controller 上的 Login 方法
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// This doesn't count login failures towards lockout only two factor authentication
// To enable password failures to trigger lockout, change to shouldLockout: true
var result = await SignInHelper.PasswordSignIn(model.Email, model.Password,
model.RememberMe,
shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresTwoFactorAuthentication:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
登录:与 Identity 1.0 对比
如果我们快速看一下在使用了 Identity 1.0 的 MVC 项目中如何处理登录任务,我们可以直接转到 AccountController.Login
方法,找到以下代码片段:
使用 Identity 1.0 的 Account Controller 上的 Login 方法
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindAsync(model.UserName, model.Password);
if (user != null)
{
await SignInAsync(user, model.RememberMe);
return RedirectToLocal(returnUrl);
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
在上面的方法中,我们调用了一个 UserManager
类,这与我们在 SignInHelper
中看到的代码类似。我们还调用了 SignInAsync
方法,该方法也直接在 AccountController
中定义。
Identity 1.0 的 Account Controller 上的 SignInAsync 方法
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(
user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(
new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
显然,随着安全、身份验证和授权相关功能的增加,复杂性也随之增加,即使是在基本的登录级别。
核心所在 - ApplicationDbContext
如果您花时间研究过 ASP.NET MVC,尤其是 Identity,您很可能熟悉 ApplicationDbContext
。这是 Entity Framework 的默认实现类,您的应用程序通过它访问和存储与 Identity 相关的数据。
在示例项目中,团队对此的设置与使用 Identity 1.0 的标准 ASP.NET 项目略有不同。首先,如果我们查看 IdentityModels.cs 文件,会发现 ApplicationDbContext
类定义如下:
Identity 2.0 示例项目中的 ApplicationDbContext 类
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false)
{
}
static ApplicationDbContext()
{
// Set the database intializer which is run once during application start
// This seeds the database with admin user credentials and admin role
Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
上面的代码设置了两个静态方法,Create()
和另一个 ApplicationDbContext()
,后者设置了一个数据库初始化程序。后者在启动时被调用,并执行 ApplicationDbInitializer
类中建立的任何数据库初始化。
如果我们回到 IdentityConfig.cs 文件,会发现 ApplicationDbInitializer
定义如下:
IdentityConfig.cs 文件中的 ApplicationDbInitializer 类
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
{
protected override void Seed(ApplicationDbContext context)
{
InitializeIdentityForEF(context);
base.Seed(context);
}
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
var userManager = HttpContext
.Current.GetOwinContext()
.GetUserManager<ApplicationUserManager>();
var roleManager = HttpContext.Current
.GetOwinContext()
.Get<ApplicationRoleManager>();
const string name = "admin@admin.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)
{
role = new IdentityRole(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);
}
}
}
当前配置下,如果模型架构(由我们的代码优先模型对象定义)发生更改,此初始化程序将在模型架构更改时删除并重新创建数据库。否则,它将继续使用现有数据库。
如果我们希望每次应用程序运行时都删除并重新创建数据库,我们可以将基类从它继承的类更改为 DropCreateDatabaseAlways<ApplicationDbContext>
。例如,在开发过程中,如果我们想每次都从一个空(或几乎空)的数据集开始进行测试,我们可能会这样做。
另外,请注意 InitializeIdentityForEF()
方法。此方法执行的功能类似于使用 EF Migrations 时的 Seed()
方法,允许我们使用一些数据初始化数据库。在这种情况下,示例项目设置了一个预定义的管理员用户、密码和角色。
好的,约翰,这些都很棒。接下来呢?
在本文中,我们大致了解了使用 ASP.NET MVC Identity 2.0 框架的项目中一些新功能和配置项的位置。还有更多内容,我们将在几篇即将发布的文章中探讨具体细节。
更新:截至 2014 年 5 月 13 日,Identity 源代码(其中一个版本)可以在 ASP.NET vNext Github 仓库中找到。请注意,由于这本质上是 ASP.NET 的 Beta 版本,此仓库中的代码可能与您在项目中安装 Identity 2.0 时运行的实际代码不符。但值得注意的是,您可以探索此代码库,至少可以了解 Identity 2.0 框架“幕后”的工作原理。
目前,请探索并运行示例项目,以便更熟悉事物的工作方式。
其他资源和感兴趣的项目
- ASP.NET Identity 2.0:自定义用户和角色
- ASP.NET Identity 2.0 扩展身份模型并使用整数键而不是字符串
- MSDN:宣布 ASP.NET Identity 2.0.0 RTM
- Codeplex 上的 Identity 2.0
- ASP.NET Identity 2.0:设置账户验证和双因素授权
- ASP.NET:理解 OWIN、Katana 和中间件管道
以下内容侧重于使用 Identity 1.0 框架