ASP.NET Core Identity 實戰(3)認證過程

若是你沒接觸過舊版Asp.Net Mvc中的 Authorize 或者 Cookie登錄,那麼你必定會疑惑 認證這個名詞,這太正式了,這到底表明這什麼?html

獲取資源以前得先過兩道關卡Authentication & Authorization

要想了解Identity中用戶登陸以後,後續的訪問時怎樣識別用戶的,那首先咱們得了解下認證(Authentication) 和受權(Authorization)的含義web

Authentication

Authentication就是認證的意思,還舉以前公園的例子,咱們拿到門票以後,去公園,入口門衛A首先要根據門票確認咱們是誰?是老王,仍是老趙,門票是否是真的,過時沒。這個過程,這個識別來訪者是誰的過程就叫作Authentication(身份認證過程)數據庫

那Authorization 又是啥?

這兩個單詞太像了,甚至他們的釋義都很像,以致於咱們在不瞭解他們的時候老是弄混他們,Authorization是受權的意思,上一小節中,門衛A識別出了咱們是誰?Ok,咱們是老李,那麼門衛B能不能讓咱們進園呢?不必定,門衛B還要再看,門票過時沒,上一步已經看過門票是否過時了,爲何還要看呢?事情是這樣的:門衛A看到門票過時了,而後在門票副卡上寫上「門票過時」四個大字,再寫上「認證失敗」,可是負責認證的門衛A沒有攔着咱們,而是繼續讓咱們前進,由於動物園可能由於活動而容許過時門票進入,或者沒有門票也能夠,那麼接下來受權的人門衛B來看門票,他根據動物園切實的狀況看,看看許可範圍,再問問後臺這我的是否是動物園的管理員。通過種種校驗,發現,雖然門票過時了,可是今天是開放日,沒門票也行,可是咱們是普通遊客,卻來到了管理員通道,因此沒讓咱們進園——受權失敗json

小結

好了,這就是認證和受權(Authentication & Authorization),兩個不一樣的事,由兩個不一樣的人(或者組件)來作cookie

  • 認證用來確認來者是誰,確認身份(確認以後可能沒有身份)
  • 受權用來確認持有此身份的來者能不能訪問當前請求的資源

如今,咱們要記住認證與受權中的一個要點session

認證只肯定用戶是誰即便認證失敗,也不會攔截用戶訪問,攔截用戶訪問發生在受權階段mvc

另外要注意的是 Authentication和Authorization並不屬於Identity的一部分,都不屬於Identity,它和Identity是相互獨立的,而後一塊兒協做。也就是說,即使咱們沒有使用Identity ,咱們有咱們本身的用戶存儲,角色等等和身份權限相關的一切,那麼咱們能夠將咱們的成員系統完美的與Asp.Net Core 進行集成,咱們能夠假設,Identity就是咱們寫的,而後將其與Asp.Net Core進行集成app

那麼爲何要將這個與Identity無關的認證過程Authentication放在這裏呢?由於它們是協做的 Authentication和Authorization本事就是要與成員系統協做的,在代碼上,他們解耦而且獨立,可是在事實邏輯上,成員系統和認證受權老是一塊兒使用的,因此一塊兒講容易理解asp.net

那麼這篇文章只講 Authentication與Authorization中的第一個 —— Authentication,先來了解一下,asp.net core 是怎樣知道咱們已經登錄的訪客是誰的async

身份認證中間件 Authentication Middleware

中間件(Middleware)講起來又是一個長長的故事,若是你徹底沒概念,那麼我建議你先簡單學習一下asp.net core 中的中間件,你只要知道它的運行原理便可

在通常的asp.net core web 項目中,咱們通常把身份認證中間件放在 靜態文件中間件以後,Mvc中間件以前

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseStaticFiles();
    app.UseAuthentication();
    app.UseMvc(routes =>
    {
        //略...
    });
}

這樣作的目的很簡單,僅對須要認證的部分作認證

在http請求到達 mvc中間件以前,也就是進入咱們寫的邏輯代碼以前,身份認證就結束了,也就是說,身份認證不能在 controller action中控制

咱們用一張圖來簡化發生在身份認證中間件中的認證過程,注意,這裏立刻要引入一個新的概念

身份認證 handler

中間件是嵌在中間件管道中的一個一個模塊,http請求有條件的流經他們

另外一個類似的東西,有不少 handler 嵌在身份認證中間件上,那麼http是流經全部的handler嗎?

Authentication Handler

Authentication Hander 顧名思義,他就是切實處理身份認證的組件,它附加在 authentication middleware 上,在請求到來時, middleware 會在全部附加在它之上的handler中選取一個用來作身份認證

那麼當咱們使用Identity時,哪些 authentication handler 被附加了呢?當請求到來時,authentication middleware 如何知道要選擇哪一個handler呢?

接下來,咱們一一解答

Identity只添加了一種類型的 handler ——CookieAuthenticationHandler

在咱們的StartUp類中的ConfigureServices方法中,咱們添加了Identity的Service

services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

但事情沒這麼簡單,在添加Identity的同時,Identity還未咱們的項目添加了AuthenticationServiceCookieAuthenticationHandler

public static IdentityBuilder AddIdentity<TUser, TRole>(
{
    // Services used by identity
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
        // 略...
    })
    .AddCookie(IdentityConstants.ApplicationScheme, o =>
    {
    // 略...

services.AddAuthentication的內部添加了AuthenticationService

namespace Microsoft.Extensions.DependencyInjection
{
    public static class AuthenticationCoreServiceCollectionExtensions
    {
         public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
        {
            services.TryAddScoped<IAuthenticationService, AuthenticationService>();
            services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
            services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();

注意上面代碼的最後三行,後面涉及到他們的獲取,他們就是在此處添加的

namespace Microsoft.Extensions.DependencyInjection
{
    public static class CookieExtensions
    {
        public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions)
        {
            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
            return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);

上方的代碼添加了 CookieAuthenticationHandler

在添加Authentication service的同時,制定默認的 authentication scheme(這個概念在以前的文章中提到過,若是你還有印象的話) 是誰(就是下方的cookie authentication handler)

這時候另外一個問題浮現了,只添加了一個 cookie authentication handler,爲何還要將他制定成默認值,是否有有點畫蛇添足呢?

雖然Identity只添加了一種類型的 handler(cookie authentication handler),可是他同時添加了多個

在 authentication 中間件上,區分各個handler的方法是指定不一樣的 authentication scheme,而不是經過 handler 的類型

其實它添加了這麼多:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
    options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme, 略)
.AddCookie(IdentityConstants.ExternalScheme, 略)
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, 略)
.AddCookie(IdentityConstants.TwoFactorUserIdScheme,略);

不過咱們暫時不用關心這些是什麼

目前爲止咱們已經知道了這樣幾件事:

  1. 添加Identity時,Identity添加了用於身份認證的服務,以及默認激活的用於認證的handler——CookieAuthenticationHandler

  2. Identity的身份認證基於cookie (上篇文章咱們瞭解到 Identity在登錄時將票據加密寫入了cookie)
  3. 因此用戶登陸後再次訪問的時候,會經過cookie來識別當前用戶是誰

動手實踐

這一小節中咱們先編寫測試代碼,來看看認證過程產生了哪些結果

建立一個名爲TestAuthController的控制器,代碼大體以下:

namespace IdentityInAction.Controllers
{
    public class TestAuthController : Controller
    {
        public IActionResult Index()
        {
            return Json(new
            {
                User.Identity.IsAuthenticated,
                User.Identity.AuthenticationType,
                Claims=User.Claims.Select(c => new { c.Type, c.Value })
                // 略...

這些代碼將返回一個json字符串,內容是 authentication的部分結果和用戶的claims信息,這三行核心代碼的意思分別是:

  • 用戶是否已經經過身份認證
  • 對次請求進行認證的handler的名稱(在上篇文章中咱們有提到 authentication scheme 就是 authentication type的另外一個名字,記住這件事對咱們的理解頗有幫助)
  • 這個用戶的Claims信息

運行程序,不要進行登錄,若是已經登錄了則退出登錄,退出登錄的連接是右上角的LogOut

而後訪問http://localhost:{你的端口}/testauth/index,獲得的結果以下:

{
  "isAuthenticated": false,
  "authenticationType": null,
  "claims": []
}

因爲沒有用戶登錄,因此結果裏幾乎什麼都沒有,而後再嘗試登錄後再次訪問這個地址,結果以下:

{
  "isAuthenticated": true,
  "authenticationType": "Identity.Application",
  "claims": [
    {
      "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
      "value": "78a032c7-0d67-4cec-b031-2d15a7bac755"
    },
    {
      "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
      "value": "abc@abc.com"
    },
    {
      "type": "AspNet.Identity.SecurityStamp",
      "value": "babbb46b-6ba0-4b87-875a-92088197dfbf"
    }
  ]
}

"isAuthenticated": true 這表明認證成功

"authenticationType": "Identity.Application"這是對該請求進行認證的handler的名字,由前文咱們知道,咱們默認的handler是名爲IdentityConstants.ApplicationScheme的cookie handler,咱們看一小段源代碼證明一下:

public class IdentityConstants
{
    private static readonly string CookiePrefix = "Identity";
    public static readonly string ApplicationScheme = CookiePrefix + ".Application";

正如所料,接下來就是claims了,再上篇文章中提到再登錄過程當中加入到Identity的claims有這些:

  • 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(若是支持的話)(注:支持,但當前用戶沒有)

這些claims隨着票據一塊兒加密寫到了cookie中,如今他們又隨着cookie一塊兒傳了回來
要注意的是,即使咱們退出登錄後沒有身份認證失敗了,可是咱們仍然得到了這個Uri的訪問權限,緣由在於認證並不阻止用戶,受權纔會阻止用戶,而咱們沒又作受權方面的限制

看到這裏,咱們的認證過程的大致已經清楚了,接下來咱們要看下整個認證過程的一點細節,整個過程是自上而下的,看標題爲工做的那一列

工做 註釋
獲取IAuthenticationHandlerProvider的實例
獲取默認的AuthenticationScheme
使用上一步的scheme獲取IAuthenticationService實例
上一步的service經過第一步的IAuthenticationHandlerProvider獲取handler handler 是 cookie authentication  handler
handler 調用 AuthenticateAsync,這個方法最終調用了handler的HandleAuthenticateAsync① 這個方法是事實上執行認證的方法
獲取 CookieAuthenticationOptions.Cookie.Name指定的存儲票據的cookie的原始字符串 這個Name的默認值是`.AspNetCore.Identity.Application`
解密cookie字符串得到AuthenticationTicket的實例
檢查是否使用了session存儲,若是有則驗證是否存在對應的session
檢查cookie 是否過時
檢查是否須要刷新cookie
建立新的AuthenticationTicket
將AuthenticationTicket中的Principal設置到HttpContext.User上,認證結束 在動手作一節中,咱們使用的User就是在這個時候被賦值的

須要注意

① 誰進行的驗證

Identity的實現比較複雜,兜兜轉轉最終的驗證時由 cookie authentication handler 的 HandleAuthenticateAsync完成的,若是你在看Identity源代碼的話,那麼直接跳轉到這裏能夠節省時間

怎麼驗證的

事實上,說的簡單一點,就是在登錄的時候,把票據加密寫到cookie裏,驗證的時候

獲取cookie >解密 >還原成票據 >把票據塞到http context中

即便是認證失敗了,也是這4個步驟,最終 負責受權的組件會檢查 http context 中的票據,還會結合其它狀況來肯定是否容許當前的請求繼續進行下去,而咱們的邏輯代碼中也能夠查看票據,根據不一樣的 認證結果 返回不一樣的數據

最後咱們貼上AuthenticationService的AuthenticateAsync源代碼,這寫代碼就是上方表格所對應的代碼,可是咱們要注意到,表格中的內容還包含這份代碼所調用的其它代碼的步驟

public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
    {
        if (scheme == null)
        {
            var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
            scheme = defaultScheme?.Name;
            if (scheme == null)
            {
                throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found.");
            }
        }

        var handler = await Handlers.GetHandlerAsync(context, scheme);
        if (handler == null)
        {
            throw await CreateMissingHandlerException(scheme);
        }

        var result = await handler.AuthenticateAsync();
        if (result != null && result.Succeeded)
        {
            var transformed = await Transform.TransformAsync(result.Principal);
            return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme));
        }
        return result;
    }

全文完

相關文章
相關標籤/搜索