上一篇文章(ASP.NET Core Identity Hands On(1)——Identity 初次體驗)中,我們初識了Identity,並且詳細分析了AspNetUsers
用戶存儲表,這篇我們將一起學習Identity 默認生成的樣板代碼的注冊與登陸過程
注冊/Register
打開AccountController
找到 public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
方法
這個方法切實的創建用戶並存儲到數據庫,完整的過程代碼比較復雜,所以我們用一張表格來展現具體過程,首先看緊挨着箭頭的那一列文本,即標題為“工作”的那一列,這是完整的順序過程,用戶創建即從頭走到尾。剩余的信息是幫助理解的,因為在Register
方法中,並沒有展現關鍵的內容,我列舉出他們出現的位置,這樣有助於理解
在看圖片之前,我們先看一下CreateAsync
代碼,這可能和你的有點不同,因為我刪除了一點無關緊要的東西來減少篇幅
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);
}
如果不太理解代碼也沒關系,我們看表格
另外值得注意的是圖中的標注①,驗證用戶名中的字符,他的默認值是
public string AllowedUserNameCharacters { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
如果我們想更改設置怎么辦?還有表格中提到了 如果用戶支持鎖定、如果要求郵件不能重復,這些未確定的值從哪來的?
如果你熟悉 asp.net core ,那我猜你可能已經想到了
沒錯 Options 就是 Di中的 Options在起作用。
打開項目根目錄的Startup.cs
文件
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 本意有
- 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<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
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/HttpAbstractions
Security*
這個庫里面主要包含用於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存入數據庫,從而完成密碼的兼容升級
②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<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
在這個步驟中大部分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<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