上一篇文章(ASP.NET Core Identity Hands On(1)——Identity 初次体验)中,我们初识了Identity,并且详细分析了AspNetUsers
用户存储表,这篇我们将一起学习Identity 默认生成的样板代码的注册与登陆过程
注册/Register
打开AccountController
找到 public async Task
方法
这个方法切实的创建用户并存储到数据库,完整的过程代码比较复杂,所以我们用一张表格来展现具体过程,首先看紧挨着箭头的那一列文本,即标题为“工作”的那一列,这是完整的顺序过程,用户创建即从头走到尾。剩余的信息是帮助理解的,因为在Register
方法中,并没有展现关键的内容,我列举出他们出现的位置,这样有助于理解
在看图片之前,我们先看一下CreateAsync
代码,这可能和你的有点不同,因为我删除了一点无关紧要的东西来减少篇幅
namespace IdentityDemo.Controllers
{
public async Task 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);
}
如果不太理解代码也没关系,我们看表格
另外值得注意的是图中的标注①,验证用户名中的字符,他的默认值是
public string AllowedUserNameCharacters { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
如果我们想更改设置怎么办?还有表格中提到了 如果用户支持锁定、如果要求邮件不能重复,这些未确定的值从哪来的?
如果你熟悉 asp.net core ,那我猜你可能已经想到了
没错 Options 就是 Di中的 Options在起作用。
打开项目根目录的Startup.cs
文件
public class Startup
{
//略...
public void ConfigureServices(IServiceCollection services)
{
//略...
services.AddIdentity()
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
//略...
}
}
当前整个identity options应用的都是默认配置,所以这里看不到option的踪迹,接下来我们就以刚才提到的三个选项为例,修改option 的值,修改后的代码如下
public class Startup
{
//略...
public void ConfigureServices(IServiceCollection services)
{
//略...
services.AddIdentity(options=>
{
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.@";
options.User.RequireUniqueEmail = false;
options.Lockout.AllowedForNewUsers = false;
})
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
//略...
}
}
允许的用户名字符由abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+
变为abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.@
(现在你再试试注册,之前可以用 _
现在不能用了)
要求邮件不重复由true
变为false
允许新用户锁定由true
变为false
IdentityOptions
可配置的选项非常多,完整的列表请移步 配置 ASP.NET 核心标识
更多关于Options的内容请移步 asp.net core 文档——配置与选项 一节
登陆之前——咱们得先弄清Claim
举个例子
假设有这样一家动物园,这家动物园要门票,门票要从动物园门口的售票室买,购买后,能得到一张纸质的票据。纸很特殊,动物园验票能通过纸张来判断门票是不是真的,还能看出你有没有涂改门票。门票上还有时间,指示什么时候门票到期,只要门票没有到期,你就可以随意进出动物园
嗯,这么长个例子,其实和Claim没什么关系 :)
门票上有什么?我们来假设一下
好了,我们假设的门票就这样,从门票的第二行(姓名...)开始,每一行都是一个Claim
有了上面的铺垫,我们接下来正式介绍下Claim
释义
Claim 本意有
- vt.声称;索取;断言;需要
- vi.提出要求
- n.索赔;声称;(根据权利而提出的)要求;断言
断言是比较准确的释义,另外可以理解成声明,每一条claim 都代表了一条票据的信息,比如示例票据上的姓名等等。claim 的基本组成是 type
和value
,上面票据中左侧的就是type
右面就是value
在 .net core 基础类库中是含有Claim的实现类的,它的位置是
System.Security.Claims.Claim
我们看一个真是的claim的例子
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
这个例子中含有3个claim
- sub subject 主题,往往指Id
- name 就是name
- iat issue at 发出时间
这个例子中的 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
方法
public async Task 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
SignInResult 只有5个属性
- Success 表示一切顺利,登陆成功
- Failed 登陆失败
- LockedOut 用户被锁定了
- NotAllowed 不允许登陆
- TwoFactorRequired 要求双因子验证
然后我们看一下具体的登陆过程,这里仍旧是一个表格,
登陆过程描述
代码范围 | 作用 |
---|---|
我们的代码 |
从用户输入获取用户名、密码、记住我 |
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,我们先来解释下这两个东西是什么
HttpAbstractions*
这是 asp.net core 中的http基础相关抽象,例如HttpRequest、HttpResponse、HttpContext等等
关于 HttpAbstractions的更多信息,可以访问它的GitHub主页 https://github.com/aspnet/Htt...
Security*
这个库里面主要包含用于web开发的安全与授权相关的中间件,在上表中 的标注⑦IAuthenticationSignInHandler
的实例,事实上就是CookieAuthenticationHandler
,在后续的文章里当我么讲到身份认证过程的时候会详细讲述身份认证中间件及handler是如何工作的
另外,还可以访问他的GitHub主页获得更多信息https://github.com/aspnet/Sec...
接下来我们解释一下上表中的标注
标注解释
①检查用户密码是否正确,以及是否需要升级
在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存入数据库,从而完成密码的兼容升级
②AccessFailedCount计数、LockoutEnd时间
ASP.NET Core Identity Hands On(1)——Identity 初次体验中有讲解
Claim、IIdentity+ClaimsIdentity、IPrincipal+ClaimsPrincipal
在过去的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 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 Claims { get; }
再看一下上面的三小段代码,你应该就能理解 Principal、Identity、Claim的关系了
③通过用户的基本信息生成Claims 及ClaimsIdentity
在这个步骤中大部分claims都被加入到 ClaimsIdentity中,如下所示(|
右侧是该claim的type)
- UserName |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
- UserId|
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
- SecurityStamp(如果支持的话)|
AspNet.Identity.SecurityStamp
- 存储在数据库中的额外Claims(如果支持的话)
这里的 claim 的type 是url,还有字符串,而之前提到的都是缩写,这是不是很令人疑惑呢?
原因是 并没有什么规定type是什么的标准,我们也可以自定义type,type的意义在于发放票据的一方和验证票据的一方知道是什么意思就可以了,所以,如上
④额外的claims 以及 AspNetUserClaims 表
现在我们 就来解析一下我们的第二张表 AspNetUserClaims
这张表相对就比较简单,这张表就是用于存储额外的属于用的claim的
其中Id是int类型,这有别于User表中Id是varchar(450)要注意一下
我们来假设一个场景
假设我们的网站有一个特殊的设置,就是在用户是男性的时候,显示一个短发logo是女性时显示一个长发logo,我们有很多方法实现,如果用claim实现的话就是相对简单的,我们将性别的的type定义为 gender, value定义为 1、2,那么在用户创建时或者创建后,为用户创建一条claim数据,假设用户是女性:
Id :10011
ClaimType :gender
ClaimValue :2
UserId :071d2a6e-ac2e-4db6-8941-372a3991b912q
当这位用户登录时,就会将这条数据加入到cookie票据中,成为其中的一条claim,而在用户后续的访问中,我们直接从cookie中拿到票据,并看到票据上写了,这为用户是一位女性,然后为其显示一个长发logo
⑤生成ClaimPrincipal
这是一个一步的操作
CalimsIdentity id = await GenerateClaimsAsync(user);
return new ClaimsPrincipal(id);
就像我们把A用户的身份证交到了A的手中,然后把A交还给了调用方,这很好理解
⑥添加认证方法Claim
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(略...)
{
services.AddAuthentication(options =>
{
// 略...
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
// 略...
})
ApplicationScheme的切实的默认值是Identity.Application
,如果你不太能理解这一小节的内容,没关系,你只需要知道表格中做了什么事就可以,关于 身份认证 authentication 是个不算简单的过程,后续会撰文专门讲解
最后就是加密和将cookie写入http响应了,这段就不展开讲了,就是一些基本操错,而加密过程和配置 密钥,后面会有单独的讲解章节
全文完 :)