ASP.NET Core 身份驗證(一)


前言

這篇文章我想帶領大家了解一下 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
本文原創授權為:署名 - 非商業性使用 - 禁止演繹,協議普通文本 | 協議法律文本


免責聲明!

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



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