前言
這篇文章我想帶領大家了解一下 ASP.NET Core 中如何進行的身份驗證,在開始之前強烈建議還沒看過我寫的 Identity 系列文章的同學先看一下。
Identity 入門系列文章:
名詞解釋
做 Web 開發的都知道 HTTP 協議是無狀態的,那么服務端如果想知道此次請求的用戶是哪個登錄的用戶,那么就需要有一種標識每次都被傳遞到服務端,那么這個標識就是我們都知道的 Cookie(這里我們先不考慮header中攜帶標識的情況),服務端根據 Cookie 中攜帶的信息進行識別的一個過程就是身份驗證,所有基於 WEB 的服務端都是如此,無關乎語言和框架。
在整個身份驗證的過程中,又分為兩個部分即認證和授權,很多同學區分不出來這兩個東西,因為這兩個單詞看起來有點像,導致經常認錯,這里我教大家一個小方法,就是記住他們的發音,使用某種方法讓發音和漢字對應起來,這樣就記住了。
Authentication [ɔ:,θenti'keiʃən] 認證
Authorization [,ɔ:θərai'zeiʃən, -ri'z-] 授權
分享一下我的方法,認證的拼音是(renzheng),其中 zheng 包含 en ,同樣的 Authentication 也包含 en,這樣我就記住了這個單詞是認證,那么另外一個就是授權了。
認證:確定用戶身份的一個過程。 注意是一個過程。
授權:確認用戶可以做哪些事情,即權限。
基於 Claims 的身份
在 ASP.NET Core 中主要是使用的基於 Claims 的身份驗證,也就是說將用戶的屬性都抽象成證件單元來表示了,通過證件單元來表示一張身份證。
我們先來回顧一下如何制造一張身份證:
//證件單元
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name,"奧巴馬"),
new Claim(ClaimTypes.NameIdentifier,"身份證號")
};
//使用證件單元創建一張身份證
var identity = new ClaimsIdentity(claims, "AuthenticationTypeXXX");
注意,在 new ClaimsIdentity 的時候第二個參數是 AuthenticationType,我在前面文章中講過這個是 載體類型,也就是實體形式的身份證,對吧?
那么,在使用程序創建一個身份的時候,需要就指定這個載體了,在HTTP驗證中,我們將載體設置為Cookies,代碼如下:
var cookie身份證 = new ClaimsIdentity(claims, "Cookies");
有了Cookie身份證,我們還需要一個攜帶者,看過之前文章的可能知道,我講 ClaimsPrincipal 的時候,一張身份證就不是代表一個人了,而是不通的身份種類,比如你可以同時是一名教師,母親,商人。如果你想證明你同時有這幾種身份的時候,你可能需要出示教師證,你孩子的出生證,法人代表的營業執照證。
所以,我們還需要制造一個人,這個人來攜帶各種證件,我們就攜帶上一步制造的 cookie身份證 吧,先攜帶這一個好了:
var 人 = new ClaimsPrincipal(cookie身份證)
我們來看一下完整的一個代碼
//證件單元
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name,"奧巴馬"),
new Claim(ClaimTypes.NameIdentifier,"身份證號")
};
//使用證件單元創建一張cookie身份證
var cookie身份證 = new ClaimsIdentity(claims, "Cookies");
//創建一個人攜帶cookie身份證
var 人 = new ClaimsPrincipal(cookie身份證)
多重身份
當一個人有多種身份的時候,這個時候可能有人會問,什么情況下會有多種身份呢?
舉個簡單的例子,上面的 cookie身份證 算是一種身份,那么我可能還有比如接入 OAuth的時候使用的 bearer身份證,接入第三方登錄時候使用過的 google身份證,facebook身份證,microsoft身份證 等等,這就叫多重身份。
多種身份種的每一種身份都有一個 AuthenticationType 對應一個認證方式,后面我會講到。
以上,我們理清楚了一個重要的邏輯關系就是:
一個人有多種身份,每個身份都有證件單元和一個認證方式組成。
接下來,你們可能就會認為我就開始介紹認證和授權了。 不,很多東西有時候和你想象的並不一樣,比如這篇文章也是,所以接下來我要講的東西是 IdentityModel
IdentityModel
IdentityModel 是一種基於 Claim 的 Identity 庫,它提供了一組類用來標識用戶身份,以及對這些東西的抽象。
有些同學可能會問,不是已經有 ClaimsIdentity 來表示用戶身份了嗎?為啥又還有其他的表示用戶身份的東西呢?
大哥,身份認證是一整套復雜的東西,包含很多組件,協議,標准,如果很簡單就學會了我還用得着寫文章教你嗎? 還是接着介紹吧。
最初,IdentityModel 是屬於 WIF(Windows Identity Foundation) 的一部分,WIF 是微軟2004年給 .NET 平台搞的一套身份驗證框架(包含Claims,Configuration,Metadata,Policy,Servicesd等等),微軟想把這個東西作為 .NET 標准框架的一部分,所以它的命名空間是 System.IdentityModel, 了解這個東西的人不是很多,不過不知道也沒關系,反正這玩意也已經被淘汰了。
在 .NET Core 中, WIF 這些套件只有 System.IdentityModel.Tokens.Jwt 被保留了下來,其他全被扔掉了,為什么呢?
原因是只有 JWT 這部分東西有用,其他的部分更多的是為以前的 Web Servics, WCF 那套分布式東西設計的,那套分布式的東西淘汰了,自然也不必要保留了。
在沒有 .NET Core 的時候,我們想實現一套標准的單點登錄(SSO)系統就可以利用 System.IdentityModel 因為它已經為我們做了大量工作,並且是標准化的。在 .NET Core 中也需要一些標准的抽象東西那怎么辦呢?
微軟弄了一套新的 IdentityModel 的庫,命名空間為 Microsoft.IdentityModel。很多人甚至都找不到它的源碼在哪里,我一開始也沒找到,最后發現在 https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet 這個倉庫里面。
這個庫的組成部分同樣都是抽象的部分,包括相關的協議對象,票據的加解密,票據存儲 等等,也就是說微軟給 .NET Core 的身份驗證體系又定義了一套抽象的東西,任何第三方基於身份驗證的實現庫或者框架都要遵循(依賴)他們。
以上的關於 IdentityModel 的介紹和下面我要將的東西關系不是很大,之所以要在這里引入是因為我要為后續的文章做鋪墊,在這里引入最合適不過。
接下來我們繼續講解,就開始了認證部分的講解。
Authentication 認證
我之前講過奧巴馬去杭州旅游的故事,有些同學反映還是看不懂,所以我決定這次配合 ASP.NET Core 中 Cookie 身份認證的過程來講解。
再次聲明,如果你還沒看過 Identity 入門一 這篇文章,我要求你先跳過去看一下,因為接下來的內容是這篇文章的延申。
我們假設你現在已經知道了人和身份證,然后現在人使用身份證是坐火車。
人就是奧巴馬
身份證就是 cookie身份證
我們將開始我們的認證旅程,同時結合我們最熟悉的 HTTP 登錄流程。
奧馬巴要去乘坐火車,那么現在他要過安檢,在Web登錄中就是對應的登錄,登錄要使用用戶名密碼,但是用戶名密碼是屬於業務邏輯方面的驗證,我們不考慮,因為假設是第三方登錄就不需要輸入用戶名和密碼了,所以你可以理解為我們假設用戶名和密碼都正確,現在奧馬巴要過安檢了。
對應的代碼為:
//證件單元
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name,"奧巴馬"),
new Claim(ClaimTypes.NameIdentifier,"身份證號")
};
//使用證件單元創建一張身份證
var identity = new ClaimsIdentity(claims,"Cookies");
//使用身份證創建一個證件當事人,也就是奧巴馬
var identityPrincipal = new ClaimsPrincipal(identity);
//奧巴馬開始過安檢
await HttpContext.SignInAsync("Cookies", identityPrincipal);
現在,我們來運行程序,看看會發生什么。你先不用管 HttpContext.SignInAsync 是做什么用的,下面會說。
新建一個ASP.NET Core 空的 MVC 程序,然后在登錄的 Action 方法中粘貼以上代碼,然后按 F5 運行。

出錯了,根據錯誤信息我們可以看出是因為我們沒有注冊身份驗證的中間件,而且錯誤已經告訴了我們應該怎么做,我們嘗試解決這個錯誤。
在 Startup.cs 文件中 ConfigureServices 方法注冊服務
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication("Cookies")
.AddCookie("Cookies");
...
}
注意,AddAuthentication 這里是指定默認的認證載體類型,AddCookie 這里是注冊載體類型的處理程序。
認證部分我會在下一篇中詳細介紹,所以這里先大致了解下。
再次 F5 運行發現已經正常了。
我們打開瀏覽器的 Cookie 查看一下,可以看到多了一項 Cookie 記錄

我們可以看到這個 Cookie 的 Name 為 .AdpNetCore.Cookie,Value 為一大長串加密的字符串。
流程講解
現在我來開始講 HttpContext.SignIn。
它是一個擴展方法,最終是調用的 IAuthenticationService 接口的 SignInAsync 方法。我們來看下接口的定義:
public interface IAuthenticationService
{
Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
}
有了接口,肯定有實現咯。 我們找一下實現在哪里,很容易,根據 ASP.NET Core 的 IOC 來找就行了,很明顯在 AddCookie 這個擴展里面。
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication()
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
↑↑↑ 實現就在這里
...
}
我們找到了處理類 CookieAuthenticationHandler 這個對象,我們再來看具體的代碼。
protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
// Process the request cookie to initialize members like _sessionKey.
await EnsureCookieTicket();
var cookieOptions = BuildCookieOptions();
var signInContext = new CookieSigningInContext(
Context,
Scheme,
Options,
user,
properties,
cookieOptions);
await Events.SigningIn(signInContext);
var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name);
if (Options.SessionStore != null)
{
if (_sessionKey != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
_sessionKey = await Options.SessionStore.StoreAsync(ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.ClaimsIssuer));
ticket = new AuthenticationTicket(principal, null, Scheme.Name);
}
var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name,
cookieValue,
signInContext.CookieOptions);
var signedInContext = new CookieSignedInContext(
Context,
Scheme,
signInContext.Principal,
signInContext.Properties,
Options);
await Events.SignedIn(signedInContext);
// Only redirect on the login path
var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
await ApplyHeaders(shouldRedirect, signedInContext.Properties);
Logger.SignedIn(Scheme.Name);
}
大概步驟分為:
1、創建一個SignIn Cookie 上下文對象
2、將上下文對象轉換為票據(Ticket),轉換為票據的目的是為了加密
3、將票據進行加密
4、將加密后的票據寫入Cookie
很有意思的是第三步,我需要展開來說下,這也結束。
在第三步加密票據的過程中可以看到有一個 if 判斷 if (Options.SessionStore != null),是做什么用的呢?
可能有些同學會有疑問,我們基於Claim的Cookie存儲假如我的證件單元很多,就會生成一個非常大的cookie,每次傳輸是有性能影響的,並且Cookie是有最大限制的,怎么辦呢?
其實解決辦法就是我們就可以開啟這個 SessionStore,將Cookie存儲在服務端例如Redis等緩存中。代碼如下:
services.AddSingleton<ITicketStore, MyRedisTicketStore>();
services.AddOptions<CookieAuthenticationOptions>("Cookies")
.Configure<ITicketStore>((o, t) => o.SessionStore = t);
現在,瀏覽器中已經存儲了用戶的身份啦。
以上就是確認用戶身份的一個過程,在這個過程中我們使用Cookie來標記用戶身份並且存儲到瀏覽器的Cookie了,這個過程就是 認證。
其實上面就是 ASP.NET Core 中的 Forms 身份驗證中的認證階段。
擴展閱讀
在不使用Cookie的時候怎么確定身份呢? 比如在 WEB API 接口中使用的就是 Access Token,這也相當於Cookie中的票據了,那么在 WEB API 中如何確定身份,流程又是怎么樣的呢?可以看后續文章。
總結
才把認證寫完發現已經這么長了,下篇再來講講授權吧。
如果你對 .NET Core 有興趣的話可以關注我,我會定期的在博客分享我的學習心得。
本文地址:http://www.cnblogs.com/savorboard/p/authentication.html
作者博客:Savorboard
本文原創授權為:署名 - 非商業性使用 - 禁止演繹,協議普通文本 | 協議法律文本
