ASP.NET Core 認證與授權[1]:初識認證


原文: ASP.NET Core 認證與授權[1]:初識認證

在ASP.NET 4.X 中,我們最常用的是Forms認證,它既可以用於局域網環境,也可用於互聯網環境,有着非常廣泛的使用。但是它很難進行擴展,更無法與第三方認證集成,因此,在 ASP.NET Core 中對認證與授權進行了全新的設計,並使用基於聲明的認證(claims-based authentication),以適應現代化應用的需求。在運行原理解剖[5]:Authentication中介紹了一下HttpContext與認證系統的集成,本系列文章則來詳細介紹一下 ASP.NET Core 中認證與授權。

目錄

  1. 基於聲明的認證
  2. ASP.NET Core 中的用戶身份
  3. Microsoft.AspNetCore.Authentication
  4. 認證Handler

基於聲明的認證

Claim 通常被翻譯成聲明,但是感覺過於生硬,還是使用Claim來稱呼更加自然一些。記得是在MVC5中,第一次接觸到 “Claim" 的概念。在MVC5之前,我們所熟悉的是Windows認證和Forms認證,Windows認證通常用於企業內部,我們使用最多的還是Forms認證,先來回顧一下,以前是怎么使用的:

首先我們會在web.config中配置認證模式:

<authentication mode="Forms">
    <forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

認證票據的生成是使用FormsAuthentication來完成的:

FormsAuthentication.SetAuthCookie("bob", true);

然后便可以通過HttpContext.User.Identity.Name獲取到當前登錄用戶的名稱:"bob",那么它是如何來完成認證的呢?

在 ASP.NET 4.x 中,我們應該都對 HttpModule 比較了解,它類似於 ASP.NET Core 中的中件間,ASP.NET 默認會在全局的 administration.config 文件中注冊一大堆HttpModule,其中就包括WindowsAuthenticationFormsAuthentication,用來實現Windows認證和Forms認證:

<moduleProviders>
    <!-- Server Modules-->
    <add name="Authentication" type="Microsoft.Web.Management.Iis.Authentication.AuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="AnonymousAuthentication" type="Microsoft.Web.Management.Iis.Authentication.AnonymousAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="BasicAuthentication" type="Microsoft.Web.Management.Iis.Authentication.BasicAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="ActiveDirectoryAuthentication" type="Microsoft.Web.Management.Iis.Authentication.ActiveDirectoryAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="WindowsAuthentication" type="Microsoft.Web.Management.Iis.Authentication.WindowsAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="DigestAuthentication" type="Microsoft.Web.Management.Iis.Authentication.DigestAuthenticationModuleProvider, Microsoft.Web.Management.Iis, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<span class="hljs-comment">&lt;!-- ASP.NET Modules--&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">add</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"FormsAuthentication"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"Microsoft.Web.Management.AspNet.Authentication.FormsAuthenticationModuleProvider, Microsoft.Web.Management.Aspnet, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"</span> /&gt;</span>        </code></pre>

可能大多人都不知道有這些Module,這也是微軟技術的一大弊端,總想着封裝成傻瓜化,造成入門容易,精通太難的局面。

如上,我們可以看到生成票據時,默認只能轉入一個Name,當然也可以通過手動創建FormsAuthenticationTicket來附帶一些額外的信息,但是都太過麻煩。

在傳統的身份認證中,每個應用程序都有它自己的驗證用戶身份的方式,以及它自己的用戶數據庫。這種方式有很大的局限性,因為它很難集成多種認證方式以支持用戶使用不同的方式來訪問我們的應用程序,比如組織內的用戶(Windows-baseed 認證),其它組織的用戶(Identity federation)或者是來自互聯網的用戶(Forms-based 認證)等等。

Claim 是關於一個人或組織的某個主題的陳述,比如:一個人的名稱,角色,個人喜好,種族,特權,社團,能力等等。它本質上就是一個鍵值對,是一種非常通用的保存用戶信息的方式,可以很容易的將認證和授權分離開來,前者用來表示用戶是/不是什么,后者用來表示用戶能/不能做什么。

因此基於聲明的認證有兩個主要的特點:

  • 將認證與授權拆分成兩個獨立的服務。

  • 在需要授權的服務中,不用再去關心你是如何認證的,你用Windows認證也好,Forms認證也行,只要你出示你的 Claims 就行了。

ASP.NET Core 中的用戶身份

Claim

在 ASP.NET Core 中,使用Cliam類來表示用戶身份中的一項信息,它由核心的TypeValue屬性構成:

public class Claim { private readonly string _type; private readonly string _value; 
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Claim</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> type, <span class="hljs-keyword">string</span> <span class="hljs-keyword">value</span></span>)
    : <span class="hljs-title">this</span>(<span class="hljs-params">type, <span class="hljs-keyword">value</span>, ClaimValueTypes.String, ClaimsIdentity.DefaultIssuer, ClaimsIdentity.DefaultIssuer, <span class="hljs-literal">null</span>, <span class="hljs-literal">null</span>, <span class="hljs-literal">null</span></span>)
</span>{
}

<span class="hljs-function"><span class="hljs-keyword">internal</span> <span class="hljs-title">Claim</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> type, <span class="hljs-keyword">string</span> <span class="hljs-keyword">value</span>, <span class="hljs-keyword">string</span> valueType, <span class="hljs-keyword">string</span> issuer, <span class="hljs-keyword">string</span> originalIssuer, ClaimsIdentity subject, <span class="hljs-keyword">string</span> propertyKey, <span class="hljs-keyword">string</span> propertyValue</span>)
</span>{
    ...
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Type =&gt; _type;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Value =&gt; _value;

}

一個Claim可以是“用戶的姓名”,“郵箱地址”,“電話”,等等,而多個Claim構成一個用戶的身份,使用ClaimsIdentity類來表示:

ClaimsIdentity

public class ClaimsIdentity : IIdentity
{    
    public virtual IEnumerable<Claim> Claims {get;}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">string</span> AuthenticationType =&gt; _authenticationType;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> IsAuthenticated =&gt; !<span class="hljs-keyword">string</span>.IsNullOrEmpty(_authenticationType);
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">string</span> Name
{
    <span class="hljs-keyword">get</span>
    {
        Claim claim = FindFirst(_nameClaimType);
        <span class="hljs-keyword">if</span> (claim != <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> claim.Value;
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
}

}

如上,其Name屬性用來查找Claims中,第一個Type為我們創建ClaimsIdentity時指定的NameClaimType的Claim的值,若未指定Type時則使用默認的ClaimTypes.Name。而IsAuthenticated只是判斷_authenticationType是否為空,_authenticationType則對應上一章中介紹的Scheme

下面,我們演示一下用戶身份的創建:

// 創建一個用戶身份,注意需要指定AuthenticationType,否則IsAuthenticated將為false。
var claimIdentity = new ClaimsIdentity("myAuthenticationType");
// 添加幾個Claim
claimIdentity.AddClaim(new Claim(ClaimTypes.Name, "bob"));
claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "bob@gmail.com"));
claimIdentity.AddClaim(new Claim(ClaimTypes.MobilePhone, "18888888888"));

如上,我們可以根據需要添加任意個的Claim,最后我們還需要再將用戶身份放到ClaimsPrincipal對象中。

ClaimsPrincipal

那么,ClaimsPrincipal是什么呢?在 ASP.NET 4.x 中我們可能對IPrincipal接口比較熟悉,在Controller中的User屬性便是IPrincipal類型:

public interface IPrincipal
{
    IIdentity Identity { get; }
    bool IsInRole(string role);
}

可以看到IPrincipal除了包含用戶身份外,還有一個IsInRole方法,用於判斷用戶是否屬於指定角色,在基於角色的授權當中便是調用此方法來實現的。

而在 ASP.NET Core 中,HttpContext直接使用的就是ClaimsPrincipal類型,而不再使用IPrincipal

public abstract class HttpContext
{
    public abstract ClaimsPrincipal User { get; set; }
}

而在ClaimsPrincipal中,可以包含多個用戶身份(ClaimsIdentity),除了對用戶身份的操作,還提供了針對Claims的查詢:

public class ClaimsPrincipal : IPrincipal
{
    private readonly List<ClaimsIdentity> _identities = new List<ClaimsIdentity>();
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ClaimsPrincipal</span>(<span class="hljs-params">IEnumerable&lt;ClaimsIdentity&gt; identities</span>) 
</span>{
    _identities.AddRange(identities);
}

<span class="hljs-comment">// 默認從_identities中查找第一個不為空的ClaimsIdentity,也可以自定義查找方式。</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> System.Security.Principal.IIdentity Identity {}

<span class="hljs-comment">// 查找_identities中是否包含類型為RoleClaimType(在創建ClaimsIdentity時指定,或者默認的ClaimTypes.Role)的Claim。</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">IsInRole</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> role</span>) </span>{}

<span class="hljs-comment">// 獲取所有身份的Claim集合</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> IEnumerable&lt;Claim&gt; Claims
{
    <span class="hljs-keyword">get</span>
    {
        <span class="hljs-keyword">foreach</span> (ClaimsIdentity identity <span class="hljs-keyword">in</span> Identities)
        {
            <span class="hljs-keyword">foreach</span> (Claim claim <span class="hljs-keyword">in</span> identity.Claims)
            {
                <span class="hljs-keyword">yield</span> <span class="hljs-keyword">return</span> claim;
            }
        }
    }
}

}

ClaimsPrincipal的創建非常簡單,只需傳入我們上面創建的用戶身份即可:

var principal = new ClaimsPrincipal(claimIdentity);

由於HTTP是無狀態的,我們通常使用Cookie,請求頭或請求參數等方式來附加用戶的信息,在網絡上進行傳輸,這就涉及到序列化和安全方面的問題。因此,還需要將principal對象包裝成AuthenticationTicket對象。

AuthenticationTicket

當我們創建完ClaimsPrincipal對象后,需要將它生成一個用戶票據並頒發給用戶,然后用戶拿着這個票據,便可以訪問受保持的資源,而在 ASP.NET Core 中,用戶票據用AuthenticationTicket來表示,如在Cookie認證中,其認證后的Cookie值便是對該對象序列化后的結果,它的定義如下:

public class AuthenticationTicket
{
    public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties properties, string authenticationScheme) {
        AuthenticationScheme = authenticationScheme;
        Principal = principal;
        Properties = properties ?? new AuthenticationProperties();
    }
    public AuthenticationTicket(ClaimsPrincipal principal, string authenticationScheme) : this(principal, properties: null, authenticationScheme: authenticationScheme) { }
    public string AuthenticationScheme { get; private set; }
    public ClaimsPrincipal Principal { get; private set; }
    public AuthenticationProperties Properties { get; private set; }
}

用戶票據除了包含上面創建的principal對象外,還需要指定一個AuthenticationScheme (通常在授權中用來驗證Scheme),並且還包含一個AuthenticationProperties對象,它主要是一些用戶票據安全方面的一些配置,如過期時間,是否持久等。

var properties = new AuthenticationProperties();
var ticket = new AuthenticationTicket(principal, properties, "myScheme");
// 加密 序列化
var token = Protect(ticket);

最后,我們可以將票據(token)寫入到Cookie中,或是也可以以JSON的形式返回讓客戶端自行保存,由於我們對票據進行了加密,可以保證在網絡中安全的傳輸而不會被篡改。

最終身份令牌的結構大概是這樣的:

claim-token

Microsoft.AspNetCore.Authentication

上面,我們介紹了身份票據的創建過程,下面就來介紹一下 ASP.NET Core 中的身份認證。

ASP.NET Core 中的認證系統具體實現在 Security 項目中,它包含 Cookie, JwtBearer, OAuth, OpenIdConnect 等:

security_src_dir

認證系統提供了非常靈活的擴展,可以讓我們很容易的實現自定義認證方式。

Usage

而對於認證系統的配置,分為兩步,也是我們所熟悉的注冊服務和配置中間件:

首先,在DI中注冊服務認證所需的服務:

public void ConfigureServices(IServiceCollection services) {
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(o =>
    {
        o.ClientId = "server.hybrid";
        o.ClientSecret = "secret";
        o.Authority = "https://demo.identityserver.io/";
        o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    });
}

最后,注冊認證中間件:

public void Configure(IApplicationBuilder app) {
    app.UseAuthentication();
}

如上,我們的系統便支持了CookieJwtBearer兩種認證方式,是不是非常簡單,在我們的應用程序中使用認證系統時,只需要調用 上一章 介紹的 HttpContext 中認證相關的擴展方法即可。

Microsoft.AspNetCore.Authentication,是所有認證實現的公共抽象類,它定義了實現認證Handler的規范,並包含一些共用的方法,如令牌加密,序列化等,AddAuthentication 便是其提供的統一的注冊認證服務的擴展方法:

AddAuthentication

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services) {
    services.AddAuthenticationCore();
    services.AddDataProtection();
    services.AddWebEncoders();
    services.TryAddSingleton<ISystemClock, SystemClock>();
    return new AuthenticationBuilder(services);
}

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, Action<AuthenticationOptions> configureOptions)
{
var builder = services.AddAuthentication();
services.Configure(configureOptions);
return builder;
}

如上,它首先會調用上一章中介紹的AddAuthenticationCore方法,然后注冊了DataProtectionWebEncoders兩個服務。而對 AuthenticationOptions 我們之前在IAuthenticationSchemeProvider也介紹過,它用來配置Scheme。

AddScheme

在上面的 AddAuthentication 中返回的是一個AuthenticationBuilder類型,所有認證Handler的注冊都是以它的擴展形式來實現的,它同時也提供了AddScheme擴展方法,使我們可以更加方便的來配置Scheme:

public class AuthenticationBuilder
{
    public AuthenticationBuilder(IServiceCollection services) => Services = services;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> IServiceCollection Services { <span class="hljs-keyword">get</span>; }

<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> AuthenticationBuilder AddScheme&lt;TOptions, THandler&gt;(<span class="hljs-keyword">string</span> authenticationScheme, Action&lt;TOptions&gt; configureOptions)
    <span class="hljs-keyword">where</span> TOptions : AuthenticationSchemeOptions, <span class="hljs-keyword">new</span>()
    <span class="hljs-keyword">where</span> THandler : AuthenticationHandler&lt;TOptions&gt;
    =&gt; AddScheme&lt;TOptions, THandler&gt;(authenticationScheme, displayName: <span class="hljs-literal">null</span>, configureOptions: configureOptions);

<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> AuthenticationBuilder AddScheme&lt;TOptions, THandler&gt;(<span class="hljs-keyword">string</span> authenticationScheme, <span class="hljs-keyword">string</span> displayName, Action&lt;TOptions&gt; configureOptions)
    <span class="hljs-keyword">where</span> TOptions : AuthenticationSchemeOptions, <span class="hljs-keyword">new</span>()
    <span class="hljs-keyword">where</span> THandler : AuthenticationHandler&lt;TOptions&gt;
{
    Services.Configure&lt;AuthenticationOptions&gt;(o =&gt;
    {
        o.AddScheme(authenticationScheme, scheme =&gt; {
            scheme.HandlerType = <span class="hljs-keyword">typeof</span>(THandler);
            scheme.DisplayName = displayName;
        });
    });
    <span class="hljs-keyword">if</span> (configureOptions != <span class="hljs-literal">null</span>)
    {
        Services.Configure(authenticationScheme, configureOptions);
    }
    Services.AddTransient&lt;THandler&gt;();
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>;
}

}

在這里的AddScheme 擴展方法只是封裝了對AuthenticationOptionsAddScheme的調用,如上面示例中的AddCookie便是調用該擴展方法來實現的。

AddRemoteScheme

看到 Remote 我們應該就可以猜到它是一種遠程驗證方式,先看一下它的定義:

public class AuthenticationBuilder
{
    public virtual AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
        where TOptions : RemoteAuthenticationOptions, new()
        where THandler : RemoteAuthenticationHandler<TOptions>
    {
        Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureSignInScheme<TOptions>>());
        return AddScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions: configureOptions);
    }
<span class="hljs-keyword">private</span> <span class="hljs-keyword">class</span> <span class="hljs-title">EnsureSignInScheme</span>&lt;<span class="hljs-title">TOptions</span>&gt; : <span class="hljs-title">IPostConfigureOptions</span>&lt;<span class="hljs-title">TOptions</span>&gt; <span class="hljs-title">where</span> <span class="hljs-title">TOptions</span> : <span class="hljs-title">RemoteAuthenticationOptions</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> AuthenticationOptions _authOptions;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">EnsureSignInScheme</span>(<span class="hljs-params">IOptions&lt;AuthenticationOptions&gt; authOptions</span>)
    </span>{
        _authOptions = authOptions.Value;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">PostConfigure</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> name, TOptions options</span>)
    </span>{
        options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme;
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">string</span>.Equals(options.SignInScheme, name, StringComparison.Ordinal))
        {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> InvalidOperationException(Resources.Exception_RemoteSignInSchemeCannotBeSelf);
        }
    }
}

}

首先使用PostConfigure模式(參見:Options[1]:Configure),對RemoteAuthenticationOptions進行驗證,要求遠程驗證中指定的SignInScheme不能為自身,這是為什么呢?后文再來解釋。然后便是直接調用上面介紹的 AddScheme 方法。

關於遠程驗證相對比較復雜,在本章中並不會太過深入的來介紹,在后續其它文章中會逐漸深入。

UseAuthentication

在上面,注冊認證中間件時,我們只需調用一個UseAuthentication擴展方法,因為它會執行我們注冊的所有認證Handler:

public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app) {
    return app.UseMiddleware<AuthenticationMiddleware>();
}

咦,它的代碼好簡單,只是注冊了一個 AuthenticationMiddleware 而已,迫不及待的想看看它的實現:

public class AuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    public IAuthenticationSchemeProvider Schemes { get; set; }
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">Invoke</span>(<span class="hljs-params">HttpContext context</span>)
</span>{
    context.Features.Set&lt;IAuthenticationFeature&gt;(<span class="hljs-keyword">new</span> AuthenticationFeature
    {
        OriginalPath = context.Request.Path,
        OriginalPathBase = context.Request.PathBase
    });

    <span class="hljs-keyword">var</span> handlers = context.RequestServices.GetRequiredService&lt;IAuthenticationHandlerProvider&gt;();
    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> scheme <span class="hljs-keyword">in</span> <span class="hljs-keyword">await</span> Schemes.GetRequestHandlerSchemesAsync())
    {
        <span class="hljs-keyword">var</span> handler = <span class="hljs-keyword">await</span> handlers.GetHandlerAsync(context, scheme.Name) <span class="hljs-keyword">as</span> IAuthenticationRequestHandler;
        <span class="hljs-keyword">if</span> (handler != <span class="hljs-literal">null</span> &amp;&amp; <span class="hljs-keyword">await</span> handler.HandleRequestAsync())
        {
            <span class="hljs-keyword">return</span>;
        }
    }

    <span class="hljs-keyword">var</span> defaultAuthenticate = <span class="hljs-keyword">await</span> Schemes.GetDefaultAuthenticateSchemeAsync();
    <span class="hljs-keyword">if</span> (defaultAuthenticate != <span class="hljs-literal">null</span>)
    {
        <span class="hljs-keyword">var</span> result = <span class="hljs-keyword">await</span> context.AuthenticateAsync(defaultAuthenticate.Name);
        <span class="hljs-keyword">if</span> (result?.Principal != <span class="hljs-literal">null</span>)
        {
            context.User = result.Principal;
        }
    }

    <span class="hljs-keyword">await</span> _next(context);
}

}

很簡單,但是很強大,不管我們是使用Cookie認證,還是Bearer認證,等等,都只需要這一個中間件,因為它會解析所有的Handler來執行。

不過,在這里,這會先判斷是否具體實現了IAuthenticationRequestHandler的Hander,優先來執行,這個是什么鬼?

查了一下,發現IAuthenticationRequestHandler是在HttpAbstractions中定義的,只是在運行原理解剖[5]:Authentication中沒有介紹到它:

public interface IAuthenticationRequestHandler : IAuthenticationHandler
{
    Task<bool> HandleRequestAsync();
}

它多了一個HandleRequestAsync方法,那么它存在的意義是什么呢?其實在Cookie認證中並沒有用到它,它通常在遠程認證(如:OAuth, OIDC等)中使用,下文再來介紹。

繼續分析上面代碼,通過調用Schemes.GetDefaultAuthenticateSchemeAsync來獲取到認證的Scheme,也就是上文提到的問題,我們必須指定默認的Scheme。

最后,調用AuthenticateAsync方法進行認證,認證成功后,為HttpContext.User賦值,至於如何解析身份令牌生成ClaimsPrincipal對象,則交給相應的Handler來處理。

認證Handler

上文中多次提到認證Handler,它由統一的AuthenticationMiddleware來調用,負責具體的認證實現,並分為本地認證與遠程認證兩種方式。

在本地驗證中,身份令牌的發放與認證通常是由同一個服務器來完成,這也是我們比較熟悉的場景,對於Cookie, JwtBearer等認證來說,都屬於是本地驗證。而當我們使用OAuth, OIDC等驗證方式時,身份令牌的發放則是由獨立的服務或是第三方(QQ, Weibo 等)認證來提供,此時在我們的應用程序中獲取身份令牌時需要請求遠程服務器,因此稱之為遠程驗證。

AuthenticationHandler

AuthenticationHandler是所有認證Handler的抽象基類,對於本地認證直接實現該類即可,定義如下:

public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new()
{
    ...
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">InitializeAsync</span>(<span class="hljs-params">AuthenticationScheme scheme, HttpContext context</span>)
</span>{
    ...

    <span class="hljs-keyword">await</span> InitializeEventsAsync();
    <span class="hljs-keyword">await</span> InitializeHandlerAsync();
}

<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">InitializeEventsAsync</span>(<span class="hljs-params"></span>) </span>{ }
<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> Task&lt;<span class="hljs-keyword">object</span>&gt; <span class="hljs-title">CreateEventsAsync</span>(<span class="hljs-params"></span>) </span>=&gt; Task.FromResult(<span class="hljs-keyword">new</span> <span class="hljs-keyword">object</span>());
<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> Task <span class="hljs-title">InitializeHandlerAsync</span>(<span class="hljs-params"></span>) </span>=&gt; Task.CompletedTask;

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;AuthenticateResult&gt; <span class="hljs-title">AuthenticateAsync</span>(<span class="hljs-params"></span>)
</span>{
    <span class="hljs-keyword">var</span> result = <span class="hljs-keyword">await</span> HandleAuthenticateOnceAsync();

    ...
}

<span class="hljs-function"><span class="hljs-keyword">protected</span> Task&lt;AuthenticateResult&gt; <span class="hljs-title">HandleAuthenticateOnceAsync</span>(<span class="hljs-params"></span>)
</span>{
    <span class="hljs-keyword">if</span> (_authenticateTask == <span class="hljs-literal">null</span>)
    {
        _authenticateTask = HandleAuthenticateAsync();
    }
    <span class="hljs-keyword">return</span> _authenticateTask;
}

<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">abstract</span> Task&lt;AuthenticateResult&gt; <span class="hljs-title">HandleAuthenticateAsync</span>(<span class="hljs-params"></span>)</span>;


<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> Task <span class="hljs-title">HandleForbiddenAsync</span>(<span class="hljs-params">AuthenticationProperties properties</span>)
</span>{
    Response.StatusCode = <span class="hljs-number">403</span>;
    <span class="hljs-keyword">return</span> Task.CompletedTask;
}

<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> Task <span class="hljs-title">HandleChallengeAsync</span>(<span class="hljs-params">AuthenticationProperties properties</span>)
</span>{
    Response.StatusCode = <span class="hljs-number">401</span>;
    <span class="hljs-keyword">return</span> Task.CompletedTask;
}

...

}

如上,它定義一個抽象方法HandleAuthenticateAsync,並使用HandleAuthenticateOnceAsync方法來保證其在每次認證只執行一次。而HandleAuthenticateAsync是認證的核心,交給具體的認證Handler負責實現。而對於 ChallengeAsync, ForbidAsync 等方法也提供了默認的實現。

而對於HandleAuthenticateAsync的實現,大致的邏輯就是從請求中獲取上面發放的身份令牌,然后解析成AuthenticationTicket,並經過一系列的驗證,最終返回ClaimsPrincipal對象。

RemoteAuthenticationHandler

RemoteAuthenticationHandler 便是所有遠程認證的抽象基類了,它繼承自AuthenticationHandler,並實現了IAuthenticationRequestHandler接口:

public abstract class RemoteAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions>, IAuthenticationRequestHandler
    where TOptions : RemoteAuthenticationOptions, new()
{
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> Task&lt;<span class="hljs-keyword">bool</span>&gt; <span class="hljs-title">ShouldHandleRequestAsync</span>(<span class="hljs-params"></span>) </span>=&gt; Task.FromResult(Options.CallbackPath == Request.Path);

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">async</span> Task&lt;<span class="hljs-keyword">bool</span>&gt; <span class="hljs-title">HandleRequestAsync</span>(<span class="hljs-params"></span>)
</span>{
    <span class="hljs-keyword">if</span> (!<span class="hljs-keyword">await</span> ShouldHandleRequestAsync())
    {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
    }

    <span class="hljs-keyword">var</span> authResult = <span class="hljs-keyword">await</span> HandleRemoteAuthenticateAsync();

    ...

    <span class="hljs-keyword">await</span> Context.SignInAsync(SignInScheme, ticketContext.Principal, ticketContext.Properties);

    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">string</span>.IsNullOrEmpty(ticketContext.ReturnUri)) ticketContext.ReturnUri = <span class="hljs-string">"/"</span>;
    Response.Redirect(ticketContext.ReturnUri);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}

<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">abstract</span> Task&lt;HandleRequestResult&gt; <span class="hljs-title">HandleRemoteAuthenticateAsync</span>(<span class="hljs-params"></span>)</span>;

<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">async</span> Task&lt;AuthenticateResult&gt; <span class="hljs-title">HandleAuthenticateAsync</span>(<span class="hljs-params"></span>)
</span>{
    <span class="hljs-keyword">var</span> result = <span class="hljs-keyword">await</span> Context.AuthenticateAsync(SignInScheme);

    ...
}

<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> Task <span class="hljs-title">HandleForbiddenAsync</span>(<span class="hljs-params">AuthenticationProperties properties</span>)
    </span>=&gt; Context.ForbidAsync(SignInScheme);

<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">GenerateCorrelationId</span>(<span class="hljs-params">AuthenticationProperties properties</span>) </span>{}
<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">ValidateCorrelationId</span>(<span class="hljs-params">AuthenticationProperties properties</span>) </span>{}

}

在上面介紹的AuthenticationMiddleware中,提到它會先執行實現了IAuthenticationRequestHandler 接口的Handler(遠程認證),之后(若未完成認證)再執行本地認證Handler。

RemoteAuthenticationHandler中核心的認證邏輯便是 HandleRequestAsync 方法,它主要包含2個步驟:

  1. 首先執行一個抽象方法HandleRemoteAuthenticateAsync,由具體的Handler來實現,該方法返回的HandleRequestResult對象包含驗證的結果(跳過,失敗,成功等),在成功時會包含一個ticket對象。

  2. 若上一步驗證成功,則根據返回的ticket,獲取到ClaimsPrincipal對象,並調用其它認證Handler的Context.SignInAsync方法。

也就是說,遠程Hander會在用戶未登錄時,指引用戶跳轉到認證服務器,登錄成功后,解析認證服務器傳回的憑證,最終依賴於本地Handler來保存身份令牌。當用戶再次訪問則無需經過遠程Handler,直接交給本地Handler來處理。

由此也可以知道,遠程認證中本身並不具備SignIn的能力,所以必須通過指定其它SignInScheme交給本地認證來完成 SignIn

對於其父類的HandleAuthenticateAsync抽象方法則定義了一個默認實現:“直接轉交給本地驗證來處理”。當我們需要定義自己的遠程認證方式時,通常只需實現 HandleRemoteAuthenticateAsync 即可,而不用再去處理 HandleAuthenticateAsync 。

總結

基於聲明的認證並不是微軟所特有的,它在國外被廣泛的使用,如微軟的ADFS,Google,Facebook,Twitter等等。在基於聲明的認證中,對認證和授權進行了明確的區分,認證用來頒發一個用戶的身份標識,其包含這個用戶的基本信息,而對於這個身份的頒發則由我們信任的第三方機構來(STS)頒發(當然,你也可以自己來頒發)。而授權,則是通過獲取身份標識中的信息,來判斷該用戶能做什么,不能做什么。

本文對 ASP.NET Core 中認證系統的整個流程做了一個簡要的介紹,可能會比較苦澀難懂,不過沒關系,大致有個印象就好,下一章則詳細介紹一下最常用的本地認證方式:Cookie認證,后續也會詳細介紹 OIDC 的用法與實現,到時再回頭來看本文或許會豁然開朗。


免責聲明!

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



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