ASP.NET Core Identity 實戰(2)——註冊、登陸、Claim

上一篇文章(ASP.NET Core Identity Hands On(1)——Identity 初次體驗)中,咱們初識了Identity,而且詳細分析了AspNetUsers用戶存儲表,這篇咱們將一塊兒學習Identity 默認生成的樣板代碼的註冊與登錄過程html

註冊/Register

打開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 本意有

  • vt.聲稱;索取;斷言;須要
  • vi.提出要求
  • n.索賠;聲稱;(根據權利而提出的)要求;斷言

斷言是比較準確的釋義,另外能夠理解成聲明,每一條claim 都表明了一條票據的信息,好比示例票據上的姓名等等。claim 的基本組成是 typevalue,上面票據中左側的就是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定義爲 一、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

相關文章
相關標籤/搜索