Asp.Net Identity学习笔记+MVC5默认项目解析_基础用法

时间:2016-08-07 21:41:51   收藏:0   阅读:8970

前言

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

ASP.NET Identity特性

Identity包

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

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

技术分享

如上图所示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"
  3. providerName="System.Data.SqlClient" />
  4. </connectionStrings>

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<IdentityUserRole>().ToTable("MyUserRoles");
  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 返回用户枚举

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

准备工作

在使用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. });

在亮出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. }


来自为知笔记(Wiz)


评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!