ASP.NET Core Identity 實戰(1)——Identity 初次體驗


ASP.NET Core Identity是用於構建ASP.NET Core Web應用程序的成員資格系統,包括成員資格、登錄和用戶數據存儲

這是來自於 ASP.NET Core Identity 倉庫主頁的官方介紹,如果你是個萌新你可能不太理解什么是成員資格,那我來解釋一下,成員資格由 membership 直譯而來, membership 還有會員資格、會員身份、會員全體等相關含義,我們可以將其簡單直接但並非十分恰當的理解為用戶管理系統

ASP.NET Core Identity(下文簡稱Identity),既然可以理解為用戶管理系統,那么她自然是十分強大的,包含用戶管理的方方面面,簡單的來講包括:

  1. 用戶數據存儲(使用任意你喜歡的關系型數據庫,從sqllite到mysql、sqlserver等等,由Entity Framwork 支持)
  2. 登陸、注冊外加身份認證(基於cookie的身份認證,如果你使用Vs那么還可以生成用於注冊登錄的用戶界面及處理代碼)
  3. 角色管理
  4. 基於聲明的認證模式Claims Based Authentication(如果你不知道Claim是什么,沒關系你先記住這個單詞)

Ok Identity這么好,她到底長啥樣?我怎么用呢,接下來我們先來做一個小小的demo體驗一下,一邊做,一邊講解

軟件准備

  • Visual Studio 2017(越新越好,如果你沒有的話就下載Vs2017社區版,安裝很快速,與舊版本兼容,完全免費傳送門

動手做

打開Vs的創建新項目面板依次選擇 .net core -> asp.net core web 應用程序

選擇 web 應用程序(模型視圖控制器)->更改身份認證->個人用戶賬戶
在這之后默認會使用 sqlserver compact來存儲用戶數據

Ctrl+F5運行項目

注意到右上角的 register 和 login了嗎?在我們選擇個人身份認證的時候 Identity被自動添加到項目中,並且生成了

  • 賬戶控制器AccountController 注冊和登陸相關的代碼都在這里)
  • 登陸注冊頁面(還有其它的 如:確認郵件、訪問受限等等)
  • 管理控制器ManageController 這是給注冊用戶用的,主要有兩個功能,改密碼和雙因子驗證)
  • Identity可不會給你生成管理員界面哦

點擊 register 進入注冊界面,界面看起來還不錯,甚至可以直接使用,然后我們注冊一個賬戶

當你點擊 register 按鈕之后,會跳轉到 數據庫遷移(如果你用過EF Core,那么這個概念你並不會感到陌生) 確認頁面

應用遷移后,你要等一會刷新頁面,在這段時間里,我建議你看看遷移頁面上的信息
如果看不太懂,那么請看下圖

Ok, 遷移好了之后,就會回到主頁,右上角的注冊登錄會變成你的郵箱和注銷鏈接,點擊你的賬戶郵箱,先看看里面有什么

這個頁面里的內容就在ManageController中,如果你不知道雙因子認證Two-factor authentication是什么,沒關系,在后續講到它時再說

點一下 Send verification email 鏈接,不用擔心,不會真的發送郵件

Identity 提供了電子郵件驗證功能,就是通常見到的那種,郵件中會有一個加密的鏈接,用於驗證郵件,如何生成鏈接Identity已經做好了,甚至寫了郵件發送的接口——IEmailSender和一個空的實現EmailSender

namespace IdentityDemo.Services
{
    public interface IEmailSender
    {
        Task SendEmailAsync(string email, string subject, string message);
    }
}

查看數據庫

剛剛我們注冊了一個新的用戶,那么用戶存哪里了?默認存儲用的是sqlserver compact,接下來我們找到它,再看看Identity是如何設計用戶數據,另外我自己粗淺的認為學習一個新技術最好就是先看看它把數據存成什么樣了

在Vs上方的菜單里依次選擇 工具->連接到數據庫

Ok,默認數據庫的位置是哪里?數據庫叫什么名字呢?現在,先關閉這個窗口,打開項目根目錄下的appsettings.json配置文件

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-IdentityDemo-E3266F7D-D9FD-4038-9AF7-773A31FC3680;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

可以看到我們數據庫的名字叫做aspnet-IdentityDemo-E3266F7D-D9FD-4038-9AF7-773A31FC3680,而他的位置在 C:\Users\{當前登錄的用戶名}\下面。 再操作一次,然后點 繼續 選擇你的數據庫文件

這可能會遇到數據庫文件占用的的情況

這是因為剛剛啟動的程序沒有退出,如果你用的是自托管啟動,那么關閉它如果用的是IISExpress,也關閉它

好了,先看看數據庫里有什么吧

_EFMigrationsHistory 是 Ef的遷移歷史表不必關注此表

AspNetUserClaimsAspNetRoleClaims是用戶和角色的聲明表,之前我們提到 Identity 是基於聲明的認證模式(Claims Based Authentication)的,Claim在其中扮演者很重要的角色,甚至角色(Role)都被轉換成了Claim,Claim相關會在后面專門講解,如果你不了解它,不要着急

AspNetUsersAspNetRolesAspNetUserRoles存儲用戶和角色信息

AspNetUserTokensAspNetUserLogins存儲的是用戶使用的外部登陸提供商的信息和Token,外部登陸提供商指的是像微博、QQ、微信、Google、微軟這類提供 oauth 或者 openid connect 登陸的廠商。比如 segmentfault 就可以使用微博登陸

接下來就要解釋下最為重要的一張表AspNetUsers

用戶數據核心存儲—— AspNetUsers

剛剛注冊的用戶的切實數據如下

博客園不支持橫向滾動代碼,所以我把代碼都豎過來了,看着有點別扭 或者你可以到這里看具體的數據

Id                                           
------------------------------------         
d4929072-e704-447c-a9aa-e1b7f510fd37         


AccessFailedCount 
----------------- 
0                 

ConcurrencyStamp                     
------------------------------------ 
5765da8f-1945-40c6-8f81-97604739e5ec 

Email              
-----------------  
xxxxxxxx@163.com   

EmailConfirmed
--------------
0             

LockoutEnabled 
-------------- 
1              

LockoutEnd
----------
NULL      

NormalizedEmail  
-----------------
XXXXXXXX@163.COM 

NormalizedUserName 
------------------ 
XXXXXXXX@163.COM   

PasswordHash                                                                        
------------------------------------------------------------------------------------
AQAAAAEAACcQAAAAEHQ+3Z9h0tiUsinNPs8B99skAqbXh0zcWlGWTgTVik6S85viEWQFV8TF8bRyDTW8rw==

PhoneNumber
-----------
NULL       

PhoneNumberConfirmed
--------------------
0                   

SecurityStamp                       
------------------------------------
a4d9c858-cc08-4ceb-8d5d-92a6cb1c40b8

TwoFactorEnabled
----------------
0               

UserName          
----------------- 
xxxxxxxx@163.com  

Id

主鍵 默認是 nvarchar(450) 但事實上是存儲的Guid字符串,另外值得一提的是Id的創建時機

主鍵的Guid是在創建用戶時在構造函數中生成的

namespace Microsoft.AspNetCore.Identity
{
    public class IdentityUser : IdentityUser<string>
    {
        public IdentityUser()
        {
            Id = Guid.NewGuid().ToString();
        }

這是一小段源代碼,用來證明上述內容

也就是說它是完全隨機的無序Guid,那么它可能帶來的隱患就是當用戶量非常大的時候,創建用戶可能變慢,不過對於絕大多數情景來講,這不太可能(有那么多的用戶),當然這可能發生,所以在后續的文章里,我會講解如何使用bigint作為主鍵

AccessFailedCount

這個是用來記錄用戶嘗試登陸卻登陸失敗的次數,我們可以通過這個來確定在什么時候需要鎖定用戶,

ConcurrencyStamp

同步標記,每當用戶記錄被更改時必須要更改此列的值,事實上存儲的是Guid,並且在創建用戶模型的時候直接在屬性上初始化隨機值

public virtual string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString();

另外要注意,這個列的值的更改時機,它是在程序中手動編寫的代碼更改的,而不是由數據庫更改(可能是考慮到並不是所有ef支持的數據庫都支持timestamp 或者 rowversion 類型)

Email、NormalizedEmail

Email就是Email,NormalizedEmail是 規范化后的Email
什么是規范化呢?
在我們剛剛創建的用戶中,可以看到 NormalizedEmail 只是將email 的值變成大寫了,我想你已經有點明白了

的確,這樣會提高數據庫的查詢效率,從Identity的代碼中可以看到,關於Email的查詢都轉換成了對 NormalizedEmail的查詢。空口無憑,我們看一小段簡短的代碼

namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
{
        public override Task<TUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
        {
            // 略...
            return Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, cancellationToken);
        }

NormalizedEmail在使用時你可以不用關心,你也不要去手動更改它的值,因為當用戶創建或者用戶資料更新的時候 NormalizedEmail都會被自動更新

然后我們依舊看一眼源代碼代碼

namespace Microsoft.AspNetCore.Identity
{
    public class UserManager<TUser> : IDisposable where TUser : class
    {
        public virtual async Task<IdentityResult> CreateAsync(TUser user)
        {
            // 略...
            await UpdateNormalizedUserNameAsync(user);
            await UpdateNormalizedEmailAsync(user);
            return await Store.CreateAsync(user, CancellationToken);
        }

        protected virtual async Task<IdentityResult> UpdateUserAsync(TUser user)
        {
            // 略...
            await UpdateNormalizedUserNameAsync(user);
            await UpdateNormalizedEmailAsync(user);
            return await Store.UpdateAsync(user, CancellationToken);
        }

UserName 、NormalizedUserName

UserName就是UserName NormalizedUserName 還是規范化之后的UserName,也就是轉換到大寫
它們的行為和上述的 Email、NormalizedEmail 一致,就不贅述了

EmailConfirmed

郵件已經確認,這是個bit(bool)類型的列,前文提到Identity含有發送和驗證確認郵件的功能,在創建用戶的時候這個值默認是false ,確認鏈接由 Identity生成,之后交由 IEmailSender發送。Ok,這里一半任務就做完了,展示一小段代碼能讓你更清楚這個過程

public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
    //略...
    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);
        略...

這些代碼是創建項目時生成的,是屬於你的項目而不是Identity的

你可能想到,在注冊之后我們順利的進入系統,而並沒有被阻止,即便我們沒有確認過郵件,數據庫中的數據也指明郵件沒有確認

的確,因為這已經不在Identity的范疇內了,這屬於我們的程序邏輯,要不要阻止未驗證郵件的用戶登錄,需要我們自己做,不過,很簡單,只需在登陸時多寫幾行代碼而已,這里暫時先不展開討論

LockoutEnabled、LockoutEnd

他們的數據類型是 bit和datetimeoffset(7),LockoutEnabled指示這個用戶可不可以被鎖定,LockoutEnd指定鎖定的到期日期,null 或者一個過去的時間,代表這個用戶沒有被鎖定

需要注意的是Identity為我們實現了鎖定功能的基礎設施,但是是否在用戶鎖定之后禁止用戶登錄是屬於我們程序的邏輯的

PasswordHash

密碼哈希,Identity使用的hash 強度是比較高的,暴力破解的難度十分大

=======================
HASHED PASSWORD FORMATS
=======================

Version 2:
PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
(See also: SDL crypto guidelines v5.1, Part III)
Format: { 0x00, salt, subkey }

Version 3:
PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
(All UInt32s are stored big-endian.)

version 2和3 是為了兼容Identity V1 V2 和V3 他們的對應關系如下

  • v1 、v2 -> Version 2
  • v3 -> Version 3

SecurityStamp

安全標記,一個隨機值,在用戶憑據相關的內容更改時,必須更改此項的值,事實存儲的是Guid
它的更改時機有:

  • 用戶創建
  • 更改用戶名
  • 移除外部登陸
  • 設置/更改郵件
  • 設置/更改電話號碼
  • 設置/更改雙因子驗證
  • 更改密碼
    ConcurrencyStamp一樣,SecurityStamp也是在程序中由代碼控制更改的

PhoneNumber、PhoneNumberConfirmed

電話和電話已確認,比較容易理解

TwoFactorEnabled

指示當前用戶是否開啟了雙因子驗證

初次體驗到此結束 😃

本文已同步發表到我的segmentfault專欄 .net core web dev
ASP.NET Core Identity Hands On(1)——Identity 初次體驗


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM