詳解ASP.NET Core 之 Identity 入門(三)
前言
最早2005年 ASP.NET 2.0 的時(shí)候開始, Web 應(yīng)用程序在處理身份驗(yàn)證和授權(quán)有了很多的變化,多了比如手機(jī)端,平板等,所以那個(gè)時(shí)候?yàn)榱诉m應(yīng)這種變化就引入了ASP.NET Membership,但是隨著時(shí)間的發(fā)展一些社交網(wǎng)站或者程序聚集了大量的用戶,比如Facebook,Twitter,QQ等,這個(gè)時(shí)候用戶希望能夠使用他們?cè)谶@些社交站點(diǎn)身份來(lái)登陸當(dāng)前網(wǎng)站,這樣可以免除注冊(cè)這些瑣碎而又必要的操作,用戶也不必記住大量的賬戶密碼。
又隨著互聯(lián)網(wǎng)的發(fā)展,越來(lái)越多的開發(fā)者不只是關(guān)注具體業(yè)務(wù)代碼的編寫,轉(zhuǎn)變?yōu)殚_始關(guān)注應(yīng)用程序代碼的單元測(cè)試,這已經(jīng)是開發(fā)者關(guān)注的核心。所以在2008年,ASP.NET 團(tuán)隊(duì)引入了 MVC 框架,這樣來(lái)幫助開發(fā)者很方便的構(gòu)建單元測(cè)試,同時(shí)開發(fā)者希望他們的 Membership 系統(tǒng)也能夠做到這一點(diǎn)。
基于以上,ASP.NET Identity 應(yīng)運(yùn)而生。
Identity 要解決的問(wèn)題
很多開發(fā)人員說(shuō)他們不愿意使用Identity,自己實(shí)現(xiàn)要方便的多,OK,那么需求來(lái)了?以下就是我針對(duì)此次任務(wù)給你提出來(lái)的需求。
身份系統(tǒng)
- 可以同時(shí)被所有的ASP.NET 框架使用(Web MVC,Web Forms,Web Api,SignalR)
- 可以應(yīng)用于構(gòu)建 Web, 手機(jī),存儲(chǔ),或者混合應(yīng)用。
能夠?qū)τ脩糍Y料(User Profile)很方便的擴(kuò)展
- 可以針對(duì)用戶資料進(jìn)行擴(kuò)展。
持久化
- 默認(rèn)把用戶信息存儲(chǔ)在數(shù)據(jù)庫(kù)中,可以支持使用EF進(jìn)行持久化。(可以看到,EF 其實(shí)只是Identity的一個(gè)功能點(diǎn)而已)
- 可以控制數(shù)據(jù)庫(kù)架構(gòu),更改表名或者主鍵的數(shù)據(jù)類型(int,string)
- 可以使用不同的存儲(chǔ)機(jī)制(如 NoSQL,DB2等)
單元測(cè)試
- 使WEB 應(yīng)用程序可以進(jìn)行單元測(cè)試,可以針對(duì)ASP.NET Identity編寫單元測(cè)試
角色機(jī)制
- 提供角色機(jī)制,可以使用不同的角色來(lái)進(jìn)行不同權(quán)限的限制,可以輕松的創(chuàng)建角色,向用戶添加角色等。
要支持基于Claims
- 需要支持基于 Claims 的身份驗(yàn)證機(jī)制,其中用戶身份是一組Claims,一組Claims可以比角色擁有更強(qiáng)的表現(xiàn)力,而角色僅僅是一個(gè)bool值來(lái)表示是不是會(huì)員而已。
第三方社交登陸
- 可以很方便的使用第三方登入,比如 Microsoft 賬戶,F(xiàn)acebook, Twitter,Google等,并且存儲(chǔ)用戶特定的數(shù)據(jù)。
封裝為中間件
- 基于中間件實(shí)現(xiàn),不要對(duì)具體項(xiàng)目產(chǎn)生依賴
- 基于 Authorzation 中間件實(shí)現(xiàn),而不是使用 FormsAuthentication 來(lái)存儲(chǔ)cookie。
NuGet包提供
- 發(fā)布為 Nuget 包,這樣可以容易的進(jìn)行迭代和bug修復(fù),可以靈活的提供給使用者。
以上,就是我提出來(lái)的需求,如果讓你來(lái)封裝這樣一個(gè)用戶身份認(rèn)證組件,你會(huì)不是想到以上的這些功能點(diǎn),那針對(duì)于這些功能點(diǎn)你又會(huì)怎么樣來(lái)設(shè)計(jì)呢?
下面來(lái)看一下 Identity 怎么樣設(shè)計(jì)的吧。
Getting Started
抽絲剝繭,我們先從入口看一下其使用方式。 首先我們打開 Startup.cs 文件,然后添加如下代碼:
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"])); services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.Cookies.ApplicationCookie.AuthenticationScheme = "ApplicationCookie"; options.Cookies.ApplicationCookie.CookieName = "Interop"; }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); } public void Configure(IApplicationBuilder app) { // 使用了 CookieAuthentication 中間件做身份認(rèn)證 app.UseIdentity(); } }
在 ConfigureServices 中,先是注冊(cè)了數(shù)據(jù)庫(kù)上下文,然后又 services.AddIdentity() 我們看一下里面都注冊(cè)了哪些服務(wù)呢?
public static IdentityBuilder AddIdentity<TUser, TRole>( this IServiceCollection services, Action<IdentityOptions> setupAction) where TUser : class where TRole : class { // 這個(gè)就是被 Identity 使用的 services.AddAuthentication(options => { // This is the Default value for ExternalCookieAuthenticationScheme options.SignInScheme = new IdentityCookieOptions().ExternalCookieAuthenticationScheme; }); // 注冊(cè) IHttpContextAccessor ,會(huì)用到 services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); // Identity services services.TryAddSingleton<IdentityMarkerService>(); services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>(); services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>(); services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>(); services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>(); services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>(); // 錯(cuò)誤描述信息 services.TryAddScoped<IdentityErrorDescriber>(); services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>(); //身份當(dāng)事人工廠 services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>(); //三大對(duì)象 services.TryAddScoped<UserManager<TUser>, UserManager<TUser>>(); services.TryAddScoped<SignInManager<TUser>, SignInManager<TUser>>(); services.TryAddScoped<RoleManager<TRole>, RoleManager<TRole>>(); if (setupAction != null) { services.Configure(setupAction); } return new IdentityBuilder(typeof(TUser), typeof(TRole), services); }
看了以上代碼后,基本上知道了 Identity 他的設(shè)計(jì)的一個(gè)架構(gòu)了,通過(guò)此結(jié)構(gòu)我們也能夠?qū)W習(xí)到我們自己封裝一個(gè)中間件的時(shí)候,該怎么樣來(lái)組織我們的代碼結(jié)構(gòu),怎么樣的利用 ASP.NET Core 給我們提供的依賴注入來(lái)更好的解耦,下面我們來(lái)看一下通過(guò)以上代碼我們能夠?qū)W到什么東西:
1、 在 public static IdentityBuilder AddIdentity<TUser, TRole>(this IServiceCollection services, Action<IdentityOptions> setupAction) 這個(gè)擴(kuò)展方法中,提供了一個(gè)參數(shù) Action<IdentityOptions>,這個(gè)是干什么用的呢? 這個(gè)是我們?cè)谠O(shè)計(jì)一個(gè)中間件的時(shí)候,有需要外部提供的參數(shù),就會(huì)設(shè)計(jì)一個(gè) Options 的類用來(lái)接受外部的參數(shù),然后封裝為一個(gè)Action委托的方式提供。在使用到的地方就可以以 IOption xxx 的形式注入進(jìn)來(lái)使用了。
2、services.TryAddScoped<Interface,Implement>(),這個(gè)注冊(cè)方式就是說(shuō),如果檢測(cè)到DI容器里面已經(jīng)有了當(dāng)前要注冊(cè)的Interface或者Service,就不會(huì)再次注冊(cè),沒有才會(huì)注冊(cè)進(jìn)去。 那么為什么此處要這樣用呢? 這是因?yàn)槿绻脩粢呀?jīng)實(shí)現(xiàn)了此接口并且已經(jīng)注冊(cè)的容器當(dāng)中的話,就使用用戶注冊(cè)的,而不是中間件自身的。用戶就能很好的對(duì)中間件提供的功能進(jìn)行自定義了,這就是OO中的多態(tài)性,這就是里氏替換原則。
3、如果你能理解第2條的話,那么你應(yīng)該知道為什么會(huì)在服務(wù)中注冊(cè) 錯(cuò)誤描述信息 那個(gè)IdentityErrorDescriber 了吧,也能夠解決你想提示賬號(hào)密碼錯(cuò)誤,無(wú)奈Identity輸出是英文問(wèn)題的提示。
4、三大對(duì)象,這個(gè)是 Identity 的核心了,所以學(xué)習(xí) Identity 的話,在看完博客 ASP.NET Core 之 Identity 入門(一,二)之后,學(xué)這三個(gè)對(duì)象就夠了。
- SignInManager: 主要處理注冊(cè)登錄相關(guān)業(yè)務(wù)邏輯。
- UserManager: 處理用戶相關(guān)添加刪除,修改密碼,添加刪除角色等。
- RoleManager:角色相關(guān)添加刪除更新等。
有些同學(xué)可能很好奇,都沒有依賴具體的數(shù)據(jù)庫(kù)或者是EF,是怎么樣做到的增刪改查的呢?
這個(gè)時(shí)候,就需要幾個(gè) Store 接口派上用場(chǎng)了。以下是Identity中定義的Store接口:
- IQueryableRoleStore
- IQueryableUserStore
- IRoleClaimStore
- IRoleStore
- IUserAuthenticationTokenStore
- IUserClaimStore
- IUserEmailStore
- IUserLockoutStore
- IUserLoginStore
- IUserPasswordStore
- IUserPhoneNumberStore
- IUserRoleStore
- IUserSecurityStampStore
- IUserStore
- IUserTwoFactorStore
有了這些接口之后,是不是豁然開朗了,原來(lái) Identity 是通過(guò)這種方式實(shí)現(xiàn)的持久化機(jī)制,依賴抽象接口而不是依賴具體的細(xì)節(jié)實(shí)現(xiàn),這就是面向?qū)ο笾械囊蕾嚨怪迷瓌t呀。
Identity 和 EntityFramework
Identity 和 EntityFramework的關(guān)系,相信上個(gè)章節(jié)看懂了之后,就很容易明白了,對(duì)的,EF 只是針對(duì)于上述 Store 接口的實(shí)現(xiàn),不信你看截圖的源碼:
Identity 打頭的那些類文件都是定義的需要持久化的Entity對(duì)象,Store結(jié)尾的那些就是接口的實(shí)現(xiàn)啦。
第三方的 Identity 實(shí)現(xiàn)
除了 EF 是官方默認(rèn)提供的持久化庫(kù)之外,還有一些第三方的庫(kù),當(dāng)然你也可以自己使用 ADO.NET 或者 Drapper 實(shí)現(xiàn)。
MangoDb 針對(duì)于 Identity 提供的實(shí)現(xiàn): https://github.com/tugberkugurlu/AspNetCore.Identity.MongoDB
LinqToDB 針對(duì)于 Identity 提供的實(shí)現(xiàn):https://github.com/linq2db/LinqToDB.Identity
總結(jié)
這篇博文寫了蠻久的時(shí)間的,一方面是因?yàn)樵跇?gòu)思怎么樣的思路來(lái)讓大家更好的理解,而不僅僅是使用。因?yàn)橛刑嗟奈恼陆榻BIdentity 的使用方式以及代碼了,但是最后大家還是不會(huì)用。后來(lái)想到如果讓別人想要理解你的庫(kù)也好代碼也好,讓其知道誕生的背景是很重要的,因?yàn)檫@才是設(shè)計(jì)的初衷。另一方面是因?yàn)镃onnect() 2016 大會(huì)上,.NET Core 發(fā)布了 1.1 版本,除了把項(xiàng)目升級(jí)到1.1之外,也在學(xué)習(xí)1.1新的一些東西,以便更好給大家分享。
相關(guān)文章
.NET?Core項(xiàng)目使用swagger開發(fā)組件
這篇文章介紹了.NET?Core項(xiàng)目使用swagger開發(fā)組件的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07ASP.NET FileUpload 上傳圖片實(shí)例
Add a FileUpload control to the aspx page2009-09-09asp.net實(shí)現(xiàn)三層架構(gòu)的例子
這篇文章主要介紹了asp.net實(shí)現(xiàn)三層架構(gòu)的例子,十分的簡(jiǎn)單實(shí)用,有需要的小伙伴可以參考下。2015-07-07.net 應(yīng)對(duì)網(wǎng)站訪問(wèn)壓力的方案總結(jié)
本文將總結(jié)下一些應(yīng)對(duì)網(wǎng)站訪問(wèn)壓力的技術(shù)方案。具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-02-02asp.net Parameters.AddWithValue方法在SQL語(yǔ)句的 Where 字句中的用法
今天晚上看論壇,有人提問(wèn)說(shuō),Parameters.AddWithValue方法在有些情況下不好使2009-01-01asp.net實(shí)現(xiàn)的計(jì)算網(wǎng)頁(yè)下載速度的代碼
剛看到有人給出asp.net實(shí)現(xiàn)的計(jì)算網(wǎng)頁(yè)下載速度的方法,本方法未經(jīng)本人測(cè)試,不知道能否可靠性如何。準(zhǔn)確來(lái)說(shuō),這只是個(gè)思路吧2013-03-03