ASP.NET MVC 随想录——开始使用ASP.NET Identity,初级篇
在之前的文章中,我为大家介绍了OWIN和Katana,有了对它们的基本了解后,才能更好的去学习ASP.NET Identity,因为它已经对OWIN 有了良好的集成。
在这篇文章中,我主要关注ASP.NET Identity的建立和使用,包括基础类的搭建和用户管理功能的实现——
在后续文章中,我将探索它更高级的用法,比如身份验证并联合ASP.NET MVC 进行授权、使用第三方登录、声明式认证等。
ASP.NET Identity 前世今生
ASP.NET Membership
在ASP.NET 2.0时代,ASP.NET Membership用于用户管理的常见需求。包括表单身份验证(Form Authentication),一个用于存储用户名、密码和其他用户信息的 SQL Server 数据库。但是现在,对于 Web 应用程序的数据存储我们有了更多的选择。而且,大多数开发者希望自己的站点能够使用第三方供应商提供的社交账号来实现身份验证和授权。但是,由于 ASP.NET Membership自身设计的限制,已经难以满足如下变化:
- 数据库架构为 SQL Server 设计,而且无法修改。虽然你可以添加额外的用户信息,但这些数据被存入了一张不同的数据表。而且这些信息难以访问,除了使用 Profile Provider API。
- 虽然通过Provider,你可以对后台数据存储结构的修改,但是该Provider的设计是假设我们对关系型数据库进行修改。虽然你也可以写一个面向非关系型(例如 Windows Azure Tables)存储机制的Provider。但是,围绕着相关的设计,你还需要大量的工作。这包括编写大量的代码,以及为那些 NoSQL 数据库不支持的方法抛出一大堆 System.NotImplementedException 异常。
- 由于登录、注销功能基于表单身份验证,因此ASP.NET Membership 无法支持 OWIN。OWIN 包括了一些用于身份验证的Middleware 中间件,如支持Microsoft 账户、 Facebook,、Google、Twitter 等的登录,还支持来自于组织内部的账号例如 Active Directory 、 Windows Azure Active Directory 等登录。OWIN 也提供了包括对OAuth 2.0, JWT 和CORS的支持。
正是由于ASP.NET Membership 诸多限制,微软采取了一系列的补救措施,比如发布了ASP.NET Simple Membership 和ASP.NET Universal Providers,他们通过Entity Framework的Code First,可以方便的去扩展用户信息,而非像ASP.NET Membership 那样需要Provider 来实现。
但是它们仍旧存在不足,主要包括如下两点:
- 对非关系型数据库支持不好
- 无法和OWIN兼容
ASP.NET Identity
由于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
- ASP.NET Identity 可以用在所有的 ASP.NET 框架上,例如 ASP.NET MVC, Web Forms,Web Pages,ASP.NET Web API 和SignalR
- ASP.NET Identity 可以用在各种应用程序中,例如Web 应用程序、移动应用、商店应用或者混合架构应用
• 易于管理用户信息
- ASP.NET Identity提供了丰富的API ,可以方便的管理用户
• 持久化控制
- 默认情况下,ASP.NET Identity将用户所有的数据存储在数据库中。ASP.NET Identity 使用 Entity Framework 实现其所有的检索和持久化机制。
- 通过Code First,你可以对数据库架构的完全控制,一些常见的任务例如改变表名称、改变主键数据类型等都可以很轻易地完成。
- 能够很容易地引入其他不同的存储机制,例如 SharePoint, Windows Azure 存储表服务, NoSQL 数据库等。不必再抛出System.NotImplementedException 异常了。
• 单元测试能力
- ASP.NET Identity 能让 Web 应用程序能够更好地进行单元测试。你可以为你应用程序使用了 ASP.NET Identity 的部分编写单元测试。
• 角色Provider
- ASP.NET Identity 中的角色Provider配合ASP.NET MVC Authorize,可以让你基于角色来限制对应用程序某个部分的访问。你可以很容易地创建Admin之类的角色,并将用户加入其中。
• 基于声明的
- ASP.NET Identity 支持基于声明的身份验证,它使用一组"声明"来表示用户的身份标识。相对于"角色","声明"能使开发人员能够更好地描述用户的身份标识。"角色"本质上只是一个布尔类型(即"属于"或"不属于"特定角色),而一个"声明"可以包含更多关于用户标识和成员资格的信息。
• 社交账号登录Provider
- 你可以很容易的为你的应用程序加入社交账号登录功能(例如 Microsoft 账户,Facebook,,Twitter,Google 等),并将用户特定的数据存入你的应用程序。
• Windows Azure Active Directory
- 你还可以加入使用 Windows Azure Active Directory 进行登录的功能,并将用户特定的数据存入你的应用程序。
• OWIN 集成
- ASP.NET 身份验证现在是基于 OWIN 中间件实现,并且可以在任何基于 OWIN 的宿主上使用。ASP.NET Identity 不依赖System.Web程序集,与此同时,它完全兼容于 OWIN 框架,并且能被用在任何基于OWIN 的Host和Server 之上。
- ASP.NET Identity使用OWIN Authentication来登录、登出操作。这意味着应用程序使用CookieAuthentication 生成 cookie 而非FormsAuthentication 。
• NuGet 包
- ASP.NET Identity 作为一个 NuGet 包进行发布,并且安装在ASP.NET MVC,Web Forms 和 ASP.NET Web API 项目模板中。当然,你也可以从 NuGet 库中下载它。
- ASP.NET Identity以NuGet包的形式发布,这样能让ASP.NET 团队更好的Bug修复和迭代新功能,与此同时,开发人员可以在第一时间获取到最新版本。
建立 ASP.NET Identity
创建 ASP.NET Identity数据库
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,能让开发者简单快速操作数据库。
添加ASP.NET Identity 包
Identity以包的形式发布在NuGet上,这能够很方便的将它安装到任意项目中,通过在Package Manger Console输入如下命令来安装Identity:
- Install-Package Microsoft.AspNet.Identity.EntityFramework
- Install-Package Microsoft.AspNet.Identity.OWIN
- Install-Package Microsoft.Owin.Host.SystemWeb
在 Visual Studio中选择创建一个完整的ASP.NET MVC项目时,默认情况下该模板会使用ASP.NET Identity API自动添加通用的用户管理模块。对于初学者,我建议学习它里面API的使用,但我不推荐将它使用在正式环境中,因为它产生了过多的通用和冗余代码,有时候我们只想让它简单工作。
更新Web.config文件
若要将ASP.NET Identity使用在项目里,除了添加相应的包之外,还需要在Web.config中添加如下配置信息:
- 数据库连接字符串
- 指定的OWIN Startup启动项,用作初始化Middleware至Pipeline
- <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>
创建Entity Framework 类
如果大家使用过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之后,接下来就是如何去使用它了,让我们再回顾一下ASP.NET Identity的几个重要知识点:
- 大多数应用程序需要用户、角色管理,ASP.NET Identity提供了API用来管理用户和身份验证
- ASP.NET Identity 可以运用到多种场景中,通过对用户、角色的管理,可以联合ASP.NET MVC Authorize 过滤器 来实现授权功能。
获取所有的Users对象
在上一小节中,通过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);
- }
创建User对象
通过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 类型对象,它包含如下了两个重要属性:
- Succeeded : 如果操作成功返回True
- Errors:返回一个字符串类型的错误集合
通过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> 然后 OverrideValidateAsync 方法,如下所示:
- 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
- };
ASP.NET Identity 其他API介绍
在上一小节中,介绍了CreateAsync 的使用,接下来一鼓作气,继续ASP.NET Identity之旅。
实现Delete 用户功能
按照我们的经验,若要删除一个用户,首先需要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之旅,探索身份验证和授权的使用,谢谢 。