0

前言

本文简单介绍Identity的使用,使用的案例是基于默认的Mvc5项目(只勾选MVC,不勾选WebApi),这个项目几乎都与Identity有关.读者可以拿着项目源码对照地看.

本人弱鸡一只,如果有哪些地方没讲述清楚请指出,如果是缺少某些背景知识抱歉本文不解释.

ASP.NET Identity特性

  • One ASP.NET Identity 系统
  • 更容易加入用户的个人资料信息
  • 持久化控制
  • 单元测试能力
  • 角色提供程序
  • 基于声明的 (Claims Based)
  • 社交账号登录提供程序 (Social Login Providers)
  • Windows Azure Active Directory
  • OWIN 集成
  • NuGet 包

Identity包

Identity是依赖于EF的Code First 和Owin的,当然你可以自己拿着Micsoft.AspNet.Identity.Core重写一份不依赖EF的Identity.

用户数据库由EF Code First创建,账号等功能通过Owin的中间件形式加入到程序中(Owin中间件相当于Asp.Net 的Module)

  • Microsoft.AspNet.Identity.EntityFramework

    这个包容纳了 ASP.NET Identity 基于 Entity Framework 的实现。它将 ASP.NET Identity 的数据和架构存入 SQL Server。

  • Microsoft.AspNet.Identity.Core

    这个包容纳了 ASP.NET Identity 的核心接口。它可以用来编写 ASP.NET Identity 的其他实现,用以支持其他持久化存储系统,如 Windows Azure 表存储, NoSQL 数据库等等。

  • Microsoft.AspNet.Identity.OWIN

    这个包为 ASP.NET 应用程序提供了将 ASP.NET Identity 引入到 OWIN 身份验证的功能。当你在为应用程序加入登录功能,调用 OWIN Cookie 身份验证中间件来生成 cookie 时,会用到这个包。

如上图所示Identity依赖了很多东西每个都是大框架,因此本文要求读者有一定的EF Code First和Owin知识

基本

Identity采用EF Code First,他内置了一些类用户创建数据库,因此 
在默认情况下Identity会创建下列表格 

Identity用的数据库上下文有点特别,是继承IdentityDbContext,正是继承了这个特殊的上下文,才会有那么多默认表

  1. public class MyIdentityDbContext : IdentityDbContext<MyIdentityUser>
  2. {
  3. //可以在这里扩展自己的表,配置数据表
  4. }

MyIdentityUser我自定义的,是实现IdentityUser接口的类

默认情况下是没有数据库的,直到创建一个新用户,EF才会去创建数据库 
这个数据库会创建在App_Data下 
 
因为在Web.config配置了数据库生成位置

  1. <connectionStrings>
  2. <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-DefaultMVC5-20160806094030.mdf;Initial Catalog=aspnet-DefaultMVC5-20160806094030;Integrated Security=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" />
  3. </connectionStrings>

注意要设置MultipleActiveResultSets=true,这个设置的意思是允许该引用程序可同时有多个活动的SqlDataReader,在后面的某些操作会出现同时多SqlDataReader情况

IdentityUser

对应数据表中AspNetUsers 
该类描述用户数据.我们先只注意用户部分忽略登入记录,角色,申明的部分

IdentityUser默认成员

名称描述
Claims返回用户的声明集合
Email返回用户的E-mail地址
Id返回用户的唯一ID
Logins返回用户的登录集合
PasswordHash返回哈希格式的用户口令,在“实现Edit特性”中会用到它
Roles返回用户所属的角色集合
PhoneNumber返回用户的电话号码
SecurityStamp返回变更用户标识时被修改的值,例如被口令修改的值
UserName返回用户名

AspNetUser表结构 

可以使用EF Code First相关的知识对默认表进行配置

  1. //改表名
  2. protected override void OnModelCreating(DbModelBuilder modelBuilder)
  3. {
  4. modelBuilder.Entity<IdentityUser>().ToTable("MyUser");
  5. }
  6. //忽略列,注意不是所有列都能忽略
  7. modelBuilder.Entity<MyIdentityUser>().Ignore(x => x.PhoneNumberConfirmed);

UserManager

用户管理类,其职责相当于业务逻辑层

名称描述
ChangePasswordAsync(id, old, new)为指定用户修改口令
CreateAsync(user)创建一个不带口令的新用户
CreateAsync(user, pass)创建一个带有指定口令的新用户
DeleteAsync(user)删除指定用户
FindAsync(user, pass)查找代表该用户的对象,并认证其口令
FindByIdAsync(id)查找与指定ID相关联的用户对象
FindByNameAsync(name)查找与指定名称相关联的用户对象,第14章“种植数据库”时会用到这个方法
UpdateAsync(user)将用户对象的修改送入数据库
Users返回用户枚举
下面的是有关角色部分本文暂不介绍
AddToRoleAsync(id, name)将指定ID的用户添加到指定name的角色
GetRolesAsync(id)返回指定ID的用户所在的角色名列表
IsInRoleAsync(id, name)如果指定ID的用户是指定name角色的成员,返回true
RemoveFromRoleAsync(id, name)在指定name角色的成员中除去指定ID的用户

同样的可以用继承的方式扩展自己的用户管理类

准备工作

在使用Identity前先要做一些配置 
首先是Owin 
默认的项目会创建一个Startup.cs,该类上的OwinStartupAttribute特性标注了该类为启动类 
这个类含有一个名称为Configuration的方法,该方法由OWIN基础架构进行调用,并为该方法传递一个Owin.IAppBuilder接口的实现,由它支持应用程序所需中间件的设置

  1. [assembly: OwinStartupAttribute(typeof(DefaultMVC5.Startup))]
  2. namespace DefaultMVC5
  3. {
  4. public partial class Startup
  5. {
  6. public void Configuration(IAppBuilder app)
  7. {
  8. ConfigureAuth(app);
  9. }
  10. }
  11. }

同时这个类是个部分类,在App_start中能找到另一部分ConfigureAuth就是用于配置Identity

  1. public void ConfigureAuth(IAppBuilder app)
  2. {
  3. app.CreatePerOwinContext(ApplicationDbContext.Create);
  4. app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
  5. //省略,先不解释
  6. }

Startup只在网站启动的时候执行,上面这段代码的CreatePerOwinContext需要传入一个委托,这个委托能返回一个对象,而这个对象在一次请求中是唯一的.所以非常时候放置类似数据库上下文之类的类. 
本质是每当用户请求时候Owin讲调用这些委托来创建对象,并把对象保存到OwinContext中.然后可以在应用程序的任何位置使用

  1. HttpContext.GetOwinContext().Get<ApplicationSignInManager>()
  2. //你可能会看到
  3. HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
  4. //这个泛型扩展方法内部调用了context.Get<TManager>();,感觉这个扩展方法只是用来打酱油的

来获得这个对象.

GetOwinContext是扩展方法,他会从HttpContext.Items中获得Owin之前保存在里面的信息,再生成OwinContext

总之使用CreatePerOwinContext可以保存一个对象在Owin上下文,使得一次请求中用到同一个对象.

ApplicationDbContext

/Models/IdentityModels.cs

数据库上下文类和用户类都是继承Identity内置类的,为了能扩展自己想要的表或表的字段

  1. public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
  2. {
  3. public ApplicationDbContext()
  4. : base("DefaultConnection", throwIfV1Schema: false)
  5. {
  6. }
  7. //给Owin用的
  8. public static ApplicationDbContext Create()
  9. {
  10. return new ApplicationDbContext();
  11. }
  12. }

ApplicationUserManager

/App_Start/IdentityConfig.cs

  1. public class ApplicationUserManager : UserManager<ApplicationUser>
  2. {
  3. public ApplicationUserManager(IUserStore<ApplicationUser> store)
  4. : base(store)
  5. {
  6. }
  7. public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
  8. {
  9. var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
  10. /*
  11. manager配置代码
  12. */
  13. return manager;
  14. }
  15. }

值得注意的是Manager的创建需要用到UserStore,如果ApplicationUserManager相等于业务层,那么他的职责相当于数据层. 
还有一点是这个Create方法的参数与ApplicationDbContext的Create不同

  1. IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context

这个Create也也是能被Owin的CreatePerOwinContext使用.参数context就是Owin上下文,Create中使用context.Get<ApplicationDbContext>获得保存在context中的ApplicationDbContext对象而不用再次手动创建

注册案例

Controllers/AccountController.cs

账号管理的相关代码在这个控制器中,你会常看到这类代码,从Owin上下文获得ApplicationUserManager对象,以便管理用户

  1. private ApplicationUserManager _userManager;
  2. public ApplicationUserManager UserManager
  3. {
  4. get
  5. {
  6. return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
  7. }
  8. private set
  9. {
  10. _userManager = value;
  11. }
  12. }
  1. [HttpPost]
  2. [AllowAnonymous]
  3. [ValidateAntiForgeryToken]
  4. public async Task<ActionResult> Register(RegisterViewModel model)
  5. {
  6. if (ModelState.IsValid)
  7. {
  8. //创建新用户
  9. var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
  10. var result = await UserManager.CreateAsync(user, model.Password);
  11. if (result.Succeeded)
  12. {
  13. //如果注册成功同时登入,SignInManager后面解释
  14. await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
  15. return RedirectToAction("Index", "Home");
  16. }
  17. AddErrors(result);
  18. }
  19. // 如果我们进行到这一步时某个地方出错,则重新显示表单
  20. return View(model);
  21. }

登入案例

用户登入后有一个比较重要的步骤让网站记住这个用户登入了(可以说是授权),传统做法会把用户数据类保存到Session中用户再请求使用查看他是否在Session保存了用户数据.而Session这种做法是利用Cookie来标识用户. 
在Identity中并不是用Session,但还是借用了Cookie 
为了开启Cookie授权在Startup类中使用这个中间件(Middleware)

  1. app.UseCookieAuthentication(new CookieAuthenticationOptions
  2. {
  3. AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
  4. LoginPath = new PathString("/Account/Login"),//当访问未授权页面时将自定跳转到这个位置
  5. CookieName = "MyCookieName",//自定义Cookie名称
  6. Provider = new CookieAuthenticationProvider
  7. {
  8. // 当用户登录时使应用程序可以验证安全戳。
  9. // 这是一项安全功能,当你更改密码或者向帐户添加外部登录名时,将使用此功能。
  10. OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
  11. validateInterval: TimeSpan.FromMinutes(30),
  12. regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
  13. }
  14. });

引用木宛城主文章的一段话 
app.UseCookieAuthentication 是一个扩展方法,它的内部帮我们做了如下几件事:

  • 使用app.Use(typeof(CookieAuthenticationMiddleware), app, options) 方法,将CookieAuthenticationMiddleware 中间件注册到OWIN Pipeline中
  • 通过app.UseStageMarker(PipelineStage.Authenticate)方法,将前面添加的CookieAuthenticationMiddleware指定在 ASP.NET 集成管道(ASP.NET integrated pipeline)的AuthenticateRequest阶段执行

当调用(Invoke)此Middleware时(在Asp.Net下Middleware是注册到Asp.Net管道中的的),将调用CreateHandler方法返回CookieAuthenticationHandler对象,它包含 AuthenticateCoreAsync方法,在这个方法中,读取并且验证Cookie,然后通过AddUserIdentity方法创建ClaimsPrincipal对象并添加到Owin环境字典中,可以通过OwinContext对象Request.User可以获取当前用户。

这是一个典型Middleware中间件使用场景,说白了就是去处理Http请求并将数据存储到OWIN环境字典中进行传递。而CookieAuthenticationMiddleware所做的事其实和FormsAuthenticationModule做的事类似。

因此用户浏览器Cookie登入时不走数据库的,用Cookie登入即使你的数据库刚刚清空你会发现也会登入成功.

在亮出MVC5默认项目代码前,我想先展示下<<Pro Asp.Net MVC5 Platform>>的代码,因为他更加的直观.

  1. [HttpPost]
  2. [AllowAnonymous]
  3. [ValidateAntiForgeryToken]
  4. public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
  5. if (ModelState.IsValid) {
  6. AppUser user = await UserManager.FindAsync(details.Name,details.Password);
  7. if (user == null) {
  8. ModelState.AddModelError("", "Invalid name or password.");
  9. } else {
  10. //获得用户的标识,所有的标识都实现IIdentity接口,这个是基于声明的标识,声明后面再讲,只要知道他与授权有关
  11. ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);
  12. AuthManager.SignOut();
  13. AuthManager.SignIn(new AuthenticationProperties {
  14. IsPersistent = false}, ident);
  15. return Redirect(returnUrl);
  16. }
  17. }
  18. ViewBag.returnUrl = returnUrl;
  19. return View(details);
  20. }

这块代码很直观,获得用户账号密码,去数据库查是否存在,如果存在就登入,顺带获得用户的声明信息.

下面是MVC5默认项目中的代码

  1. [HttpPost]
  2. [AllowAnonymous]
  3. [ValidateAntiForgeryToken]
  4. public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
  5. {
  6. if (!ModelState.IsValid)
  7. {
  8. return View(model);
  9. }
  10. // 这不会计入到为执行帐户锁定而统计的登录失败次数中
  11. // 若要在多次输入错误密码的情况下触发帐户锁定,请更改为 shouldLockout: true
  12. var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
  13. switch (result)
  14. {
  15. case SignInStatus.Success:
  16. return RedirectToLocal(returnUrl);
  17. case SignInStatus.LockedOut:
  18. return View("Lockout");
  19. case SignInStatus.RequiresVerification:
  20. return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
  21. case SignInStatus.Failure:
  22. default:
  23. ModelState.AddModelError("", "无效的登录尝试。");
  24. return View(model);
  25. }
  26. }

这份代码中并没有上面那样直观,它用了SignInManager,这个是个ApplicationSignInManager类,很容易猜到他是自定义的类,继承自SignInManager(Identity内置的).该类是利用UserManager执行一系列登入操作 
其实内部实现大致就与上上段代码一样,也是查找用户判断是否存在….

但它做的更多,单PasswordSignInAsync这个方法它不仅负责查询用户,登入用户,还负责记录用户登入记录(登入失败几次,对于被锁定用户的处理…).

用户信息验证

任何用户输入都需要验证,用户信息更是如此. 
在默认项目中不仅利用了MVC内置的模型验证,还利用了Identity的验证器. 
就拿注册来说,首先自定义了ViewModel,并打上验证特性

  1. public class RegisterViewModel
  2. {
  3. [Required]
  4. [EmailAddress]
  5. [Display(Name = "电子邮件")]
  6. public string Email { get; set; }
  7. [Required]
  8. [StringLength(100, ErrorMessage = "{0} 必须至少包含 {2} 个字符。", MinimumLength = 6)]
  9. [DataType(DataType.Password)]
  10. [Display(Name = "密码")]
  11. public string Password { get; set; }
  12. [DataType(DataType.Password)]
  13. [Display(Name = "确认密码")]
  14. [Compare("Password", ErrorMessage = "密码和确认密码不匹配。")]
  15. public string ConfirmPassword { get; set; }
  16. }

这里的验证能配合HtmlHelper实现客户端校验. 
其次利用Identity的验证器,关键点在下面代码第一行,尝试登入,如果失败的话把result中的错误信息返回给前端,AddErrors方法添加的是模型级错误,通过@Html.ValidationSummary()能显示出来

  1. var result = await UserManager.CreateAsync(user, model.Password);
  2. if (result.Succeeded)
  3. {
  4. await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
  5. return RedirectToAction("Index", "Home");
  6. }
  7. AddErrors(result);
  8. ......
  9. private void AddErrors(IdentityResult result)
  10. {
  11. foreach (var error in result.Errors)
  12. {
  13. ModelState.AddModelError("", error);
  14. }
  15. }

用户名密码验证器

App_Start/ApplicationUserManager/Create

  1. // 配置用户名的验证逻辑
  2. manager.UserValidator = new UserValidator<ApplicationUser>(manager)
  3. {
  4. AllowOnlyAlphanumericUserNames = false,
  5. RequireUniqueEmail = true
  6. };
  7. // 配置密码的验证逻辑
  8. manager.PasswordValidator = new PasswordValidator
  9. {
  10. RequiredLength = 6,
  11. RequireNonLetterOrDigit = true,
  12. RequireDigit = true,
  13. RequireLowercase = true,
  14. RequireUppercase = true,
  15. };

PasswordValidator属性定义

名称描述
RequiredLength指定合法口令的最小长度
RequireNonLetterOrDigit当设置为true时,合法口令必须含有非字母和数字的字符
RequireDigit当设置为true时,合法口令必须含有数字
RequireLowercase当设置为true时,合法口令必须含有小写字母
RequireUppercase当设置为true时,合法口令必须含有大写字母

UserValidator属性定义

名称描述
AllowOnlyAlphanumericUserNames当为true时,用户名只能含有字母数字字符
RequireUniqueEmail当为true时,邮件地址必须唯一

配置验证器后就能在有UserManager的地方使用它UserManager.PasswordValidator.ValidateAsync
通常SignInAsync这些方法内部都会调用他们的.

自定义验证器

自定义用户验证器

  1. public class CustomUserValidator : UserValidator<AppUser> {
  2. public CustomUserValidator(AppUserManager mgr) : base(mgr) {
  3. }
  4. public override async Task<IdentityResult> ValidateAsync(AppUser user) {
  5. //使用内建验证策略
  6. IdentityResult result = await base.ValidateAsync(user);
  7. //在此基础上添加自己的验证策略
  8. if (!user.Email.ToLower().EndsWith("@example.com")) {
  9. var errors = result.Errors.ToList();
  10. errors.Add("Only example.com email addresses are allowed");
  11. result = new IdentityResult(errors);
  12. }
  13. return result;
  14. }
  15. }

自定义口令验证器

  1. public class CustomPasswordValidator : PasswordValidator {
  2. public override async Task<IdentityResult> ValidateAsync(string pass) {
  3. IdentityResult result = await base.ValidateAsync(pass);
  4. if (pass.Contains("12345")) {
  5. var errors = result.Errors.ToList();
  6. errors.Add("Passwords cannot contain numeric sequences");
  7. result = new IdentityResult(errors);
  8. }
  9. return result;
  10. }
  11. }



转自: http://www.cnblogs.com/Recoding/p/5747135.html
关闭 返回顶部
联系我们
Copyright © 2011. 聚财吧. All rights reserved.