在之前的文章中,我为大家介绍了OWIN和Katana,有了对它们的基本了解后,才能更好的去学习ASP.NET Identity,因为它已经对OWIN 有了良好的集成。 在这篇文章中,我主要关注ASP.NET Identity的建立和使用,包括基础类的搭建和用户管理功能的实现—— 点此进行预览 点此下载示例代码 在后续文章中,我将探索它更高级的用法,比如身份验证并联合ASP.NET MVC 进行授权、使用第三方登录、声明式认证等。
在ASP.NET 2.0时代,ASP.NET Membership用于用户管理的常见需求。包括表单身份验证(Form Authentication),一个用于存储用户名、密码和其他用户信息的 SQL Server 数据库。但是现在,对于 Web 应用程序的数据存储我们有了更多的选择。而且,大多数开发者希望自己的站点能够使用第三方供应商提供的社交账号来实现身份验证和授权。但是,由于 ASP.NET Membership自身设计的限制,已经难以满足如下变化:
正是由于ASP.NET Membership 诸多限制,微软采取了一系列的补救措施,比如发布了ASP.NET Simple Membership 和ASP.NET Universal Providers,他们通过Entity Framework的Code First,可以方便的去扩展用户信息,而非像ASP.NET Membership 那样需要Provider 来实现。
但是它们仍旧存在不足,主要包括如下两点:
由于ASP.NET Membership、ASP.NET Simple Membership 、ASP.NET Universal Providers 设计上的不足,微软在接受了大量反馈后,于.NET Framework 4.5 中推出了ASP.NET Identity,如果用一句话概括——ASP.NET Identity 为ASP.NET 应用程序提供了一系列的API用来管理和维护用户 ,它包括如下新特性:
• One ASP.NET Identity
• 易于管理用户信息
• 持久化控制
• 单元测试能力
• 角色Provider
• 基于声明的
• 社交账号登录Provider
• Windows Azure Active Directory
• OWIN 集成
• NuGet 包
ASP.NET Identity并不像ASP.NET Membership那样依赖SQL Server架构,但关系型存储仍然是默认和最简单的实现方式,尽管近些年来NoSQL发展迅猛,但关系型数据库易于理解,仍旧是开发团队内部主流的存储选择。
ASP.NET Identity使用Entity Framework Code First来自动创建数据库架构。在此示例中,我使用localdb来创建一个空的数据库IdentityDb,然后交由Code First管理数据库架构。
localdb内置在Visual Studio中而且它是轻量级的SQL Server,能让开发者简单快速操作数据库。
Identity以包的形式发布在NuGet上,这能够很方便的将它安装到任意项目中,通过在Package Manger Console输入如下命令来安装Identity:
在 Visual Studio中选择创建一个完整的ASP.NET MVC项目时,默认情况下该模板会使用ASP.NET Identity API自动添加通用的用户管理模块。对于初学者,我建议学习它里面API的使用,但我不推荐将它使用在正式环境中,因为它产生了过多的通用和冗余代码,有时候我们只想让它简单工作。
若要将ASP.NET Identity使用在项目里,除了添加相应的包之外,还需要在Web.config中添加如下配置信息:
<connectionStrings>
<add name="IdentityDb" providerName="System.Data.SqlClient"
connectionString="Data Source=(localdb)\v11.0;Initial Catalog=IdentityDb;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False; MultipleActiveResultSets=True" />
</connectionStrings>
<appSettings>
<add key="owin:AppStartup" value="UsersManagement.IdentityConfig" />
</appSettings>
如果大家使用过ASP.NET Membership,对比过后你会发现在ASP.NET Identity扩展User信息是多么的简单和方便。
1.创建 User 类
第一个要被创建的类它代表用户,我将它命名为AppUser,继承自Microsoft.AspNet.Identity.EntityFramework 名称空间下IdentityUser,IdentityUser 提供了基本的用户信息,如Email、PasswordHash、UserName、PhoneNumber、Roles等,当然我们也可以在其派生类中添加额外的信息,代码如下:
using Microsoft.AspNet.Identity.EntityFramework;
namespace UsersManagement.Models
{
public class AppUser:IdentityUser
{
}
}
2.创建 Database Context 类
接下来的步骤就是创建EF Database Context 来操作AppUser。ASP.NET Identity将使用Code First 来创建和管理数据库架构。值得注意的是,Database Context必须继承自IdentityDbContext<T>,而且T为User类(在此示例即AppUser),代码如下所示:
public class AppIdentityDbContext : IdentityDbContext<AppUser>
{
public AppIdentityDbContext() : base("IdentityDb")
{
}
static AppIdentityDbContext()
{
Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit());
}
public static AppIdentityDbContext Create()
{
return new AppIdentityDbContext();
}
}
public class IdentityDbInit : DropCreateDatabaseIfModelChanges<AppIdentityDbContext>
{
protected override void Seed(AppIdentityDbContext context)
{
PerformInitialSetup(context);
base.Seed(context);
}
public void PerformInitialSetup(AppIdentityDbContext context)
{
//初始化
}
}
上述代码中,AppIdentityDbContext 的构造函数调用基类构造函数并将数据库连接字符串的Name作为参数传递,它将用作连接数据库。同时,当Entity Framework Code First成功创建数据库架构后,AppIdentityDbContext的静态构造函数调用Database.SetInitializer方法Seed 数据库而且只执行一次。在这儿,我的Seed 类IdentityDbInit。
最后,AppIdentityDbContext 定义了 Create方法,它将被 OWIN Middleware回掉然后返回AppIdentityDbContext实例,这个实例被存储在OwinContext中。
3.创建User Manger 类
User Manager类作为ASP.NET Identity中最为重要的类之一,用来管理User。同样,自定义的User Manger类必须继承自UserManager<T >,此处T就为AppUser。UserManager<T>提供了创建和操作用户的一些基本方法并且全面支持C# 异步编程,所以你可以使用CreateAsync(Create),FindAsync(Find)、DeleteAsync(Delete)、UpdateAsync(Update)来进行用户管理,值得注意的是,它并不通过Entity Framework 来直接操作用户,而是间接调用UserStore来实现。UserStore<T>是Entity Framework 类并实现了IUserStore<T>接口,并且实现了定义在UserManger中操作用户的方法。代码如下所示:
/// <summary>
/// 用户管理
/// </summary>
public class AppUserManager : UserManager<AppUser> {
public AppUserManager(IUserStore<AppUser> store)
: base(store) {
}
public static AppUserManager Create(
IdentityFactoryOptions<AppUserManager> options,
IOwinContext context) {
AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
//UserStore<T> 是 包含在 Microsoft.AspNet.Identity.EntityFramework 中,它实现了 UserManger 类中与用户操作相关的方法。
//也就是说UserStore<T>类中的方法(诸如:FindById、FindByNameAsync...)通过EntityFramework检索和持久化UserInfo到数据库中
AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db));
return manager;
}
}
上述代码中,静态的Create方法将返回AppUserManger实例,它用来操作和管理用户,值得注意的是,它需要传入OwinContext对象,通过该上下文对象,获取到存储在Owin环境字典中的Database Context实例。
4.创建OWIN Startup 类
最后,通过Katana(OWIN的实现)提供的API,将Middleware 中间件注册到Middleware中,如下所示:
public class IdentityConfig
{
public void Configuration(IAppBuilder app)
{
//1.使用app.Use方法将IdentityFactoryMiddleware和参数callback回掉函数注册到Owin Pipeline中
//app.Use(typeof(IdentityFactoryMiddleware<T, IdentityFactoryOptions<T>>), args);
//2.当IdentityFactoryMiddleware中间件被Invoke执行时,执行callback回掉函数,返回具体实例Instance
//TResult instance = ((IdentityFactoryMiddleware<TResult, TOptions>) this).Options.Provider.Create(((IdentityFactoryMiddleware<TResult, TOptions>) this).Options, context);
//3.将返回的实例存储在Owin Context中
//context.Set<TResult>(instance);
app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create);
app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
});
}
}
上述代码中,通过CreatePerOwinContext方法将AppIdentityDbContext和 AppUserManager的实例注册到OwinContext中,这样确保每一次请求都能获取到相关ASP.NET Identity对象,而且还能保证全局唯一。
UseCookieAuthentication 方法指定了身份验证类型为ApplicationCookie,同时指定LoginPath属性,当Http请求内容认证不通过时重定向到指定的URL。
成功建立ASP.NET Identity之后,接下来就是如何去使用它了,让我们再回顾一下ASP.NET Identity的几个重要知识点:
在上一小节中,通过CreatePerOwinContext方法将AppIdentityDbContext和 AppUserManager的实例注册到OwinContext中,我们可以通过OwinContext对象的Get方法来获取到他们,将下面代码放在Controller中,方便供Action获取对象:
private AppUserManager UserManager
{
get { return HttpContext.GetOwinContext().GetUserManager<AppUserManager>(); }
}
在上述代码中,通过Microsoft.Owin.Host.SystemWeb 程序集,为HttpContext增加了扩展方法GetOwinContext,返回的 OwinContext对象是对Http请求的封装,所以GetOwinContext方法可以获取到每一次Http请求的内容。接着通过IOwinContext的扩展方法GetUserManager获取到存储在OwinContext中的UserManager实例。
然后,通过UserManager的Users属性,可以获取到所有的User集合,如下所示:
public ActionResult Index()
{
return View(UserManager.Users);
}
通过UserManager的CreateAsync方法,可以快速的创建User对象,如下代码创建了User ViewModel:
public class UserViewModel
{
[Required]
public string Name { get; set; }
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}
使用UserManager对象的CreateAsync方法将AppUser对象将它持久化到数据库:
[HttpPost]
public async Task<ActionResult> Create(UserViewModel model)
{
if (ModelState.IsValid)
{
var user = new AppUser {UserName = model.Name, Email = model.Email};
//传入Password并转换成PasswordHash
IdentityResult result = await UserManager.CreateAsync(user,
model.Password);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
AddErrorsFromResult(result);
}
return View(model);
}
CreateAsync返回IdentityResult 类型对象,它包含如下了两个重要属性:
通过AddErrorsFromResult 方法将错误集合展示在页面上 @Html.ValidationSummary 处,如下所示:
private void AddErrorsFromResult(IdentityResult result)
{
foreach (string error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
有时候,我们需要实现密码策略,如同AD中控制那样,密码复杂度越高,那么它被破译的概率就越低。
ASP.NET Identity 提供了PasswordValidator类,提供了如下属性来配置密码策略:
RequiredLength | 指定有效的密码最小长度 |
---|---|
RequireNonLetterOrDigit | 当为True时,有效的密码必须包含一个字符,它既不是数字也不是字母 |
RequireDigit | 当为True时,有效密码必须包含数字 |
RequireLowercase | 当为True时,有效密码必须包含一个小写字符 |
RequireUppercase | 当为True时,有效密码必须包含一个大写字符 |
如果这些预定义属性无法满足我们的需求时,我们可以添加自定义的密码验证策略,只要继承PasswordValidator 并且Override ValidateAsync方法即可,如下代码所示:
public class CustomPasswordValidator : PasswordValidator
{
public override async Task<IdentityResult> ValidateAsync(string password)
{
IdentityResult result = await base.ValidateAsync(password);
if (password.Contains("12345"))
{
List<string> errors = result.Errors.ToList();
errors.Add("密码不能包含连续数字");
result = new IdentityResult(errors);
}
return result;
}
}
上述代码中,值得注意的是,IdentityResult 对象的 Errors是只读的,所以无法直接赋值,只能通过实例化IdentityResult 类并通过构造函数传入Errors。
自定义的密码策略创建完毕过后,接着就将它附加到UserManager对象的PasswordValidator 属性上,如下代码所示:
//自定义的Password Validator
manager.PasswordValidator = new CustomPasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = false,
RequireDigit = false,
RequireLowercase = true,
RequireUppercase = true
};
UserManager 除了PasswordValidator之外,还提供了一个更加通用的属性:UserValidator ,它包含如下两个策略属性:
AllowOnlyAlphanumericUserNames | 当为True时,UserName只能包含字母数字 |
---|---|
RequireUniqueEmail | 当为True时,Email地址必须唯一 |
当然这两种策略如果不满足我们的需求的话,我们也可以像Password那样去定制化,只要 继承UserValidator<T> 然后 Override ValidateAsync 方法,如下所示:
public class CustomUserValidator : UserValidator<AppUser>
{
public CustomUserValidator(AppUserManager mgr)
: base(mgr)
{
}
public override async Task<IdentityResult> ValidateAsync(AppUser user)
{
IdentityResult result = await base.ValidateAsync(user);
if (!user.Email.ToLower().EndsWith("@jkxy.com"))
{
List<string> errors = result.Errors.ToList();
errors.Add("Email 地址只支持jkxy域名");
result = new IdentityResult(errors);
}
return result;
}
}
上述代码增强了对Email的验证,必须为@jkxy域名,然后将自定义的UserValidator 附加到User Manger 对象上:
//自定义的User Validator
manager.UserValidator = new CustomUserValidator(manager) {
AllowOnlyAlphanumericUserNames = true,
RequireUniqueEmail = true
};
在上一小节中,介绍了CreateAsync 的使用,接下来一鼓作气,继续ASP.NET Identity之旅。
按照我们的经验,若要删除一个用户,首先需要Find 它。通过UserManager 对象的 FindByIdAsync来找到要被删除的对象,如果该对象不为null,那么再调用UserManager对象的DeleteAsync来删除它,如下所示:
[HttpPost]
public async Task<ActionResult> Delete(string id)
{
AppUser user = await UserManager.FindByIdAsync(id);
if (user != null)
{
IdentityResult result = await UserManager.DeleteAsync(user);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
return View("Error", result.Errors);
}
return View("Error", new[] {"User Not Found"});
}
因为编辑操作UpdateAsync 只接受一个参数,而不像CreateAsync那样可以传入Password,所以我们需要手动的去校验并给PasswordHash属性赋值,当密码策略验证通过时再去验证Email策略,这样确保没有脏数据,如下所示:
[HttpPost]
public async Task<ActionResult> Edit(string id, string email, string password)
{
//根据Id找到AppUser对象
AppUser user = await UserManager.FindByIdAsync(id);
if (user != null)
{
IdentityResult validPass = null;
if (password != string.Empty)
{
//验证密码是否满足要求
validPass = await UserManager.PasswordValidator.ValidateAsync(password);
if (validPass.Succeeded)
{
user.PasswordHash = UserManager.PasswordHasher.HashPassword(password);
}
else
{
AddErrorsFromResult(validPass);
}
}
//验证Email是否满足要求
user.Email = email;
IdentityResult validEmail = await UserManager.UserValidator.ValidateAsync(user);
if (!validEmail.Succeeded)
{
AddErrorsFromResult(validEmail);
}
if ((validEmail.Succeeded && validPass == null) || (validEmail.Succeeded && validPass.Succeeded))
{
IdentityResult result = await UserManager.UpdateAsync(user);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
AddErrorsFromResult(result);
}
}
else
{
ModelState.AddModelError("", "无法找到改用户");
}
return View(user);
}
在这篇文章中,我为大家介绍了什么是ASP.NET Identity以及怎样配置和创建它的基础类,然后演示使用API 进行用户的管理。在下一篇文章中,继续ASP.NET Identity之旅,探索身份验证和授权的使用,谢谢 。