上一篇文章(ASP.NET Core Identity Hands On(1)——Identity 初次体验)中,咱们初识了Identity,而且详细分析了AspNetUsers
用户存储表,这篇咱们将一块儿学习Identity 默认生成的样板代码的注册与登录过程html
打开AccountController
找到 public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
方法git
这个方法切实的建立用户并存储到数据库,完整的过程代码比较复杂,因此咱们用一张表格来展示具体过程,首先看紧挨着箭头的那一列文本,即标题为“工做”的那一列,这是完整的顺序过程,用户建立即从头走到尾。剩余的信息是帮助理解的,由于在Register
方法中,并无展示关键的内容,我列举出他们出现的位置,这样有助于理解github
在看图片以前,咱们先看一下CreateAsync
代码,这可能和你的有点不一样,由于我删除了一点可有可无的东西来减小篇幅web
namespace IdentityDemo.Controllers { public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null) { if (ModelState.IsValid) { var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl); await _signInManager.SignInAsync(user, isPersistent: false); return RedirectToLocal(returnUrl); } AddErrors(result); } // If we got this far, something failed, redisplay form return View(model); }
若是不太理解代码也不要紧,咱们看表格redis
另外值得注意的是图中的标注①,验证用户名中的字符,他的默认值是数据库
public string AllowedUserNameCharacters { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
若是咱们想更改设置怎么办?还有表格中提到了 若是用户支持锁定、若是要求邮件不能重复,这些未肯定的值从哪来的?segmentfault
若是你熟悉 asp.net core ,那我猜你可能已经想到了后端
没错 Options 就是 Di中的 Options在起做用。安全
打开项目根目录的Startup.cs
文件cookie
public class Startup { //略... public void ConfigureServices(IServiceCollection services) { //略... services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); //略... } }
当前整个identity options应用的都是默认配置,因此这里看不到option的踪影,接下来咱们就以刚才提到的三个选项为例,修改option 的值,修改后的代码以下
public class Startup { //略... public void ConfigureServices(IServiceCollection services) { //略... services.AddIdentity<ApplicationUser, IdentityRole>(options=> { options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.@"; options.User.RequireUniqueEmail = false; options.Lockout.AllowedForNewUsers = false; }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); //略... } }
容许的用户名字符由abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+
变为abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.@
(如今你再试试注册,以前能够用 _
如今不能用了)
要求邮件不重复由true
变为false
容许新用户锁定由true
变为false
IdentityOptions
可配置的选项很是多,完整的列表请移步 配置 ASP.NET 核心标识
更多关于Options的内容请移步 asp.net core 文档——配置与选项 一节
假设有这样一家动物园,这家动物园要门票,门票要从动物园门口的售票室买,购买后,能获得一张纸质的票据。纸很特殊,动物园验票能经过纸张来判断门票是否是真的,还能看出你有没有涂改门票。门票上还有时间,指示何时门票到期,只要门票没有到期,你就能够随意进出动物园
嗯,这么长个例子,其实和Claim没什么关系 :)
门票上有什么?咱们来假设一下
好了,咱们假设的门票就这样,从门票的第二行(姓名...)开始,每一行都是一个Claim
有了上面的铺垫,咱们接下来正式介绍下Claim
Claim 本意有
断言是比较准确的释义,另外能够理解成声明,每一条claim 都表明了一条票据的信息,好比示例票据上的姓名等等。claim 的基本组成是 type
和value
,上面票据中左侧的就是type
右面就是value
在 .net core 基础类库中是含有Claim的实现类的,它的位置是
System.Security.Claims.Claim
咱们看一个真实的claim的例子
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
这个例子中含有3个claim
这个例子中的 type 都是 JWT RFC中的标准jwt claim,上面这个例子是一个jwt票据的一部分,而在identity 中,默认使用的是cookie 身份认证,因此使用的不是 jwt 票据,而是加密cookie票据(identity没有这样定义,这样写是为了和jwt票据区分开),可是票据里面的内容,jwt和 加密cookie都是同样的都是——“claim”
再回顾下 claim是什么? 就是一条一条的 type-value 键值对,里面存储了身份证实信息
而承载claim的东西就是票据,票据有不少种 jwt 和cookie 都是主流,不过应用场景不同,by the way 票据的英文名称是“token” ,你须要记住它,后续的文章中,咱们会学习如何同时使用支持移动后端验证(jwt token)以及仅仅使用 jwt token
依旧在AccountController
中,咱们找到public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
方法
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) { var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { return RedirectToLocal(returnUrl); } if (result.RequiresTwoFactor) { return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe }); } if (result.IsLockedOut) { return RedirectToAction(nameof(Lockout)); } else { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return View(model); } }
这是个简略版本的代码,只保留了关键信息
用于登录的代码只有一行var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
但里面作的事情但是很是多的,咱们稍后在讲,如今咱们先要了解一下,登录以后有哪些结果产生——result
SignInResult 只有5个属性
而后咱们看一下具体的登录过程,这里仍旧是一个表格,
代码范围 | 做用 |
---|---|
咱们的代码 |
从用户输入获取用户名、密码、记住我 |
Identity | 检查是否须要确认邮件以及此用户邮件是否已经确认 |
检查是否支持锁定用户以及此用户是否已被锁定 |
|
检查用户密码是否正确,以及是否须要升级① |
|
若是支持锁定用户,而且支持在登录失败超过指定次数锁定用户则增长AccessFailedCount计数,而且在到达设置的计数上限后清零计数设置LockoutEnd时间② | |
经过用户的基本信息生成Claims 及ClaimsIdentity③ | |
若是支持额外的Claims存储则添加额外的Claims④ 【注:Identity支持,额外的Claims存储在AspNetUserClaims表中】 |
|
生成ClaimsPrinciple⑤ | |
添加认证方法Claim⑥ |
|
HttpAbstractions | 确保上一个单元格中的认证方法不是空 |
经过认证方法,获取指定的IAuthenticationSignInHandler实例⑦ |
|
Security | 使用ClaimsPrinciple建立 票据 |
加密票据 |
|
将加密后的票据添加到http响应的cookie头中 |
上表就是登录过程,Identity默认使用cookie做为 claims 的载体,在最后的步骤中将含有claims的票据加密存储到cookie中,这样在登录以后再次访问就能够验证cookie来识别当前是否有用户登陆,以及登录用户的身份
在代码范围一列中,咱们看到有4列,这和注册过程当中相比,多出了 HttpAbstractions 和 Security,咱们先来解释下这两个东西是什么
这是 asp.net core 中的http基础相关抽象,例如HttpRequest、HttpResponse、HttpContext等等
关于 HttpAbstractions的更多信息,能够访问它的GitHub主页 https://github.com/aspnet/HttpAbstractions
这个库里面主要包含用于web开发的安全与受权相关的中间件,在上表中 的标注⑦IAuthenticationSignInHandler
的实例,事实上就是CookieAuthenticationHandler
,在后续的文章里当我么讲到身份认证过程的时候会详细讲述身份认证中间件及handler是如何工做的
另外,还能够访问他的GitHub主页得到更多信息https://github.com/aspnet/Security
接下来咱们解释一下上表中的标注
在ASP.NET Core Identity Hands On(1)——Identity 初次体验 中,咱们有提到 Identity的密码哈希有两个版本 v2和v3,那么若是一个旧的Identity升级到新的Identity那么密码会不兼容,因此在Identity中密码验证为了兼容旧版,作了一些特殊处理。v3的密码byte以0x01开头,而v2以0x00开头,从这里能够判断出密码哈希是哪一个版本的而后根据不一样的版原本验证密码,密码验证有3个结果——失败、成功、成功且须要更新版本:
namespace Microsoft.AspNetCore.Identity { public enum PasswordVerificationResult { Failed = 0, Success = 1, SuccessRehashNeeded = 2 略...
当验证结果是SuccessRehashNeeded
时,就会从新计算新的密码Hash存入数据库,从而完成密码的兼容升级
ASP.NET Core Identity Hands On(1)——Identity 初次体验中有讲解
在过去的asp.net mvc 以及如今的新的 asp.net mvc core中,HttpContext都有个User属性,可能不少开发者都没有使用过它
namespace Microsoft.AspNetCore.Http { public abstract class HttpContext { public abstract ClaimsPrincipal User { get; set; }
因此,你暂时将ClaimsPrincipal理解成User就能够,而ClaimsPrincipal中有两个重要的属性
namespace System.Security.Claims public class ClaimsPrincipal : IPrincipal { public virtual IEnumerable<ClaimsIdentity> Identities { get; } public virtual IIdentity Identity { get; }
Identities是这个Principal(user)拥有的全部Identity,Identity 是这个Principal(user)拥有的最重要的Identity,而这个Identity的实际类型是ClaimsIdentity
,这里就至关于Principal是用户,而Identity是用户的身份证,身份证里面记录的是这个用户的我的信息,也就是claims
namespace System.Security.Claims { public class ClaimsIdentity : IIdentity { public virtual IEnumerable<Claim> Claims { get; }
再看一下上面的三小段代码,你应该就能理解 Principal、Identity、Claim的关系了
在这个步骤中大部分claims都被加入到 ClaimsIdentity中,以下所示(|
右侧是该claim的type)
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
AspNet.Identity.SecurityStamp
这里的 claim 的type 是url,还有字符串,而以前提到的都是缩写,这是否是很使人疑惑呢?
缘由是 并无什么规定type是什么的标准,咱们也能够自定义type,type的意义在于发放票据的一方和验证票据的一方知道是什么意思就能够了,因此,如上
如今咱们 就来解析一下咱们的第二张表 AspNetUserClaims
这张表相对就比较简单,这张表就是用于存储额外的属于用的claim的
其中Id是int类型,这有别于User表中Id是varchar(450)要注意一下
咱们来假设一个场景
假设咱们的网站有一个特殊的设置,就是在用户是男性的时候,显示一个短发logo是女性时显示一个长发logo,咱们有不少方法实现,若是用claim实现的话就是相对简单的,咱们将性别的的type定义为 gender, value定义为 一、2,那么在用户建立时或者建立后,为用户建立一条claim数据,假设用户是女性:
Id :10011 ClaimType :gender ClaimValue :2 UserId :071d2a6e-ac2e-4db6-8941-372a3991b912q
当这位用户登陆时,就会将这条数据加入到cookie票据中,成为其中的一条claim,而在用户后续的访问中,咱们直接从cookie中拿到票据,并看到票据上写了,这为用户是一位女性,而后为其显示一个长发logo
这是一个一步的操做
CalimsIdentity id = await GenerateClaimsAsync(user); return new ClaimsPrincipal(id);
就像咱们把A用户的身份证交到了A的手中,而后把A交还给了调用方,这很好理解
Principal.Identities.First().AddClaim(new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod));
这一步是将使用的认证方法添加到了 Identity中,它的type 是
http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod
不过登录过程当中,这个值是null,因此他没有真的添加到Identity中
在表格中咱们能看到⑥ 和⑦的范围已经再也不Identity里了,因此Identity的任务已经结束了,Identity就把用户Principal作好,身份证Identity作好,身份证上的信息Claim填好,就结束了。接下来选择哪一个用于用户登陆的handler,handler怎么作才能让用户登陆,Identity就不知道了,由于Identity是成员系统,而用户登陆属于web框架,举一个反例,不用Identity就不能使用cookie登录了吗?答案显然不是的,因此成员系统知道用户是谁,将用户信息作成一个票据,交给web框架
离开 Identity以后第一件事就是确保上一个单元格中的认证方法不是空,但是刚刚明明说了,它是null
没错当它是null 的时候,会去寻找默认的authentication schema(这是认证方法的另外一个名字),在startup 类中,注册Identity的服务时,Identity还注册了cookie authentication handler 顺便还添加了 默认的 authentication scheme 咱们看一个精简版的代码片断
public static IdentityBuilder AddIdentity<TUser, TRole>(略...) { services.AddAuthentication(options => { // 略... options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme; }) .AddCookie(IdentityConstants.ApplicationScheme, o => { // 略... })
ApplicationScheme的切实的默认值是Identity.Application
,若是你不太能理解这一小节的内容,不要紧,你只须要知道表格中作了什么事就能够,关于 身份认证 authentication 是个不算简单的过程,后续会撰文专门讲解
最后就是加密和将cookie写入http响应了,这段就不展开讲了,就是一些基本操错,而加密过程和配置 密钥,后面会有单独的讲解章节
全文完 :)
本文已同步发表到个人segmentfault专栏 .net core web dev
ASP.NET Core Identity Hands On(2)——注册、登陆、Claim