前言
ASP.NET Identity特性
Identity包
基本
IdentityUser
UserManager
准备工作
ApplicationDbContext
ApplicationUserManager
注册案例
登入案例
用户信息验证
用户名密码验证器
自定义验证器
前言
本文简单介绍Identity的使用,使用的案例是基于默认的Mvc5项目(只勾选MVC,不勾选WebApi).读者可以拿着项目源码对照地看.
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,正是继承了这个特殊的上下文,才会有那么多默认表
public class MyIdentityDbContext : IdentityDbContext<MyIdentityUser>
{
//可以在这里扩展自己的表,配置数据表
}
MyIdentityUser我自定义的,是实现IdentityUser接口的类
默认情况下是没有数据库的,直到创建一个新用户,EF才会去创建数据库
这个数据库会创建在App_Data下
因为在Web.config配置了数据库生成位置
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-DefaultMVC5-20160806094030.mdf;Initial Catalog=aspnet-DefaultMVC5-20160806094030;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
IdentityUser
对应数据表中AspNetUsers
该类描述用户数据.我们先只注意用户部分忽略登入记录,角色,申明的部分
IdentityUser默认成员
名称 | 描述 |
---|---|
Claims | 返回用户的声明集合 |
返回用户的E-mail地址 | |
Id | 返回用户的唯一ID |
Logins | 返回用户的登录集合 |
PasswordHash | 返回哈希格式的用户口令,在“实现Edit特性”中会用到它 |
Roles | 返回用户所属的角色集合 |
PhoneNumber | 返回用户的电话号码 |
SecurityStamp | 返回变更用户标识时被修改的值,例如被口令修改的值 |
UserName | 返回用户名 |
AspNetUser表结构
可以使用EF Code First相关的知识对默认表进行配置
//改表名
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityUserRole>().ToTable("MyUserRoles");
}
//忽略列,注意不是所有列都能忽略
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接口的实现,由它支持应用程序所需中间件的设置
[assembly: OwinStartupAttribute(typeof(DefaultMVC5.Startup))]
namespace DefaultMVC5
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
同时这个类是个部分类,在App_start中能找到另一部分ConfigureAuth
就是用于配置Identity
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
//省略,先不解释
}
Startup只在网站启动的时候执行,上面这段代码的CreatePerOwinContext需要传入一个委托,这个委托能返回一个对象,而这个对象在一次请求中是唯一的.所以非常时候放置类似数据库上下文之类的类.
本质是每当用户请求时候Owin讲调用这些委托来创建对象,并把对象保存到OwinContext中.然后可以在应用程序的任何位置使用
HttpContext.GetOwinContext().Get<ApplicationSignInManager>()
//你可能会看到
HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
//这个泛型扩展方法内部调用了context.Get<TManager>();,感觉这个扩展方法只是用来打酱油的
来获得这个对象.
GetOwinContext是扩展方法,他会从HttpContext.Items中获得Owin之前保存在里面的信息,再生成OwinContext
总之使用CreatePerOwinContext可以保存一个对象在Owin上下文,使得一次请求中用到同一个对象.
ApplicationDbContext
/Models/IdentityModels.cs
数据库上下文类和用户类都是继承Identity内置类的,为了能扩展自己想要的表或表的字段
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
//给Owin用的
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
ApplicationUserManager
/App_Start/IdentityConfig.cs
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>()));
/*
manager配置代码
*/
return manager;
}
}
值得注意的是Manager的创建需要用到UserStore
,如果ApplicationUserManager相等于业务层,那么他的职责相当于数据层.
还有一点是这个Create方法的参数与ApplicationDbContext的Create不同
IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context
这个Create也也是能被Owin的CreatePerOwinContext使用.参数context就是Owin上下文,Create中使用context.Get<ApplicationDbContext>
获得保存在context中的ApplicationDbContext对象而不用再次手动创建
注册案例
Controllers/AccountController.cs
账号管理的相关代码在这个控制器中,你会常看到这类代码,从Owin上下文获得ApplicationUserManager对象,以便管理用户
private ApplicationUserManager _userManager;
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
//创建新用户
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
//如果注册成功同时登入,SignInManager后面解释
await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// 如果我们进行到这一步时某个地方出错,则重新显示表单
return View(model);
}
登入案例
用户登入后有一个比较重要的步骤让网站记住这个用户登入了(可以说是授权),传统做法会把用户数据类保存到Session中用户再请求使用查看他是否在Session保存了用户数据.而Session这种做法是利用Cookie来标识用户.
在Identity中并不是用Session,但还是借用了Cookie
为了开启Cookie授权在Startup类中使用这个中间件(Middleware)
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),//当访问未授权页面时将自定跳转到这个位置
CookieName = "MyCookieName",//自定义Cookie名称
Provider = new CookieAuthenticationProvider
{
// 当用户登录时使应用程序可以验证安全戳。
// 这是一项安全功能,当你更改密码或者向帐户添加外部登录名时,将使用此功能。
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
在亮出MVC5默认项目代码前,我想先展示下<<Pro Asp.Net MVC5 Platform>>
的代码,因为他更加的直观.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
if (ModelState.IsValid) {
AppUser user = await UserManager.FindAsync(details.Name,details.Password);
if (user == null) {
ModelState.AddModelError("", "Invalid name or password.");
} else {
//获得用户的标识,所有的标识都实现IIdentity接口,这个是基于声明的标识,声明下文再讲,只要知道他与授权有关
ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);
AuthManager.SignOut();
AuthManager.SignIn(new AuthenticationProperties {
IsPersistent = false}, ident);
return Redirect(returnUrl);
}
}
ViewBag.returnUrl = returnUrl;
return View(details);
}
这块代码很直观,获得用户账号密码,去数据库查是否存在,如果存在就登入,顺带获得用户的声明信息.
下面是MVC5默认项目中的代码
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// 这不会计入到为执行帐户锁定而统计的登录失败次数中
// 若要在多次输入错误密码的情况下触发帐户锁定,请更改为 shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "无效的登录尝试。");
return View(model);
}
}
这份代码中并没有上面那样直观,它用了SignInManager,这个是个ApplicationSignInManager
类,很容易猜到他是自定义的类,继承自SignInManager(Identity内置的).该类是利用UserManager执行一系列登入操作
其实内部实现大致就与上上段代码一样,也是查找用户判断是否存在….
但它做的更多,单PasswordSignInAsync
这个方法它不仅负责查询用户,登入用户,还负责记录用户登入记录(登入失败几次,对于被锁定用户的处理…).
用户信息验证
任何用户输入都需要验证,用户信息更是如此.
在默认项目中不仅利用了MVC内置的模型验证,还利用了Identity的验证器.
就拿注册来说,首先自定义了ViewModel,并打上验证特性
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "电子邮件")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "{0} 必须至少包含 {2} 个字符。", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "密码")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "确认密码")]
[Compare("Password", ErrorMessage = "密码和确认密码不匹配。")]
public string ConfirmPassword { get; set; }
}
这里的验证能配合HtmlHelper实现客户端校验.
其次利用Identity的验证器,关键点在下面代码第一行,尝试登入,如果失败的话把result中的错误信息返回给前端,AddErrors方法添加的是模型级错误,通过@Html.ValidationSummary()
能显示出来
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
return RedirectToAction("Index", "Home");
}
AddErrors(result);
......
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
用户名密码验证器
App_Start/ApplicationUserManager/Create
// 配置用户名的验证逻辑
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// 配置密码的验证逻辑
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
PasswordValidator属性定义
名称 | 描述 |
---|---|
RequiredLength | 指定合法口令的最小长度 |
RequireNonLetterOrDigit | 当设置为true时,合法口令必须含有非字母和数字的字符 |
RequireDigit | 当设置为true时,合法口令必须含有数字 |
RequireLowercase | 当设置为true时,合法口令必须含有小写字母 |
RequireUppercase | 当设置为true时,合法口令必须含有大写字母 |
UserValidator属性定义
名称 | 描述 |
---|---|
AllowOnlyAlphanumericUserNames | 当为true时,用户名只能含有字母数字字符 |
RequireUniqueEmail | 当为true时,邮件地址必须唯一 |
配置验证器后就能在有UserManager的地方使用它UserManager.PasswordValidator.ValidateAsync
.
通常SignInAsync这些方法内部都会调用他们的.
自定义验证器
自定义用户验证器
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("@example.com")) {
var errors = result.Errors.ToList();
errors.Add("Only example.com email addresses are allowed");
result = new IdentityResult(errors);
}
return result;
}
}
自定义口令验证器
public class CustomPasswordValidator : PasswordValidator {
public override async Task<IdentityResult> ValidateAsync(string pass) {
IdentityResult result = await base.ValidateAsync(pass);
if (pass.Contains("12345")) {
var errors = result.Errors.ToList();
errors.Add("Passwords cannot contain numeric sequences");
result = new IdentityResult(errors);
}
return result;
}
}