ASP.NET Core 認證與授權[1]:初識認證 (筆記)


原文鏈接:  https://www.cnblogs.com/RainingNight/p/introduce-basic-authentication-in-asp-net-core.html

 

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

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

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

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

ASP.NET Core 中的用戶身份

Claim

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

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

public class ClaimsIdentity : IIdentity
{    
    public virtual IEnumerable<Claim> Claims {get;}

    public virtual string AuthenticationType => _authenticationType;
    public virtual bool IsAuthenticated => !string.IsNullOrEmpty(_authenticationType);
    public virtual string Name
    {
        get
        {
            Claim claim = FindFirst(_nameClaimType);
            if (claim != null) return claim.Value;
            return null;
        }
    }

}

如上,其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

ASP.NET Core 中,HttpContext直接使用的就是ClaimsPrincipal類型

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

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

var principal = new ClaimsPrincipal(claimIdentity);

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

 

AuthenticationTicket

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

用戶票據除了包含上面創建的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 項目中,它包含 CookieJwtBearerOAuthOpenIdConnect 等:

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兩種認證方式

Microsoft.AspNetCore.Authentication,是所有認證實現的公共抽象類,它定義了實現認證Handler的規范,並包含一些共用的方法,如令牌加密,序列化等,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:在這里的AddScheme 擴展方法只是封裝了對AuthenticationOptionsAddScheme的調用,如上面示例中的AddCookie便是調用該擴展方法來實現的。

AddRemoteScheme

看到 Remote 我們應該就可以猜到它是一種遠程驗證方式

UseAuthentication

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

public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app)
{
    return app.UseMiddleware<AuthenticationMiddleware>();
}
public class AuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    public IAuthenticationSchemeProvider Schemes { get; set; }

    public async Task Invoke(HttpContext context)
    {
        context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
        {
            OriginalPath = context.Request.Path,
            OriginalPathBase = context.Request.PathBase
        });

        var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
        foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
        {
            var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
            if (handler != null && await handler.HandleRequestAsync())
            {
                return;
            }
        }

        var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
        if (defaultAuthenticate != null)
        {
            var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
            if (result?.Principal != null)
            {
                context.User = result.Principal;
            }
        }

        await _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()
{
    ...

    public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
    {
        ...

        await InitializeEventsAsync();
        await InitializeHandlerAsync();
    }

    protected virtual async Task InitializeEventsAsync() { }
    protected virtual Task<object> CreateEventsAsync() => Task.FromResult(new object());
    protected virtual Task InitializeHandlerAsync() => Task.CompletedTask;

    public async Task<AuthenticateResult> AuthenticateAsync()
    {
        var result = await HandleAuthenticateOnceAsync();

        ...
    }

    protected Task<AuthenticateResult> HandleAuthenticateOnceAsync()
    {
        if (_authenticateTask == null)
        {
            _authenticateTask = HandleAuthenticateAsync();
        }
        return _authenticateTask;
    }

    protected abstract Task<AuthenticateResult> HandleAuthenticateAsync();


    protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = 403;
        return Task.CompletedTask;
    }

    protected virtual Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = 401;
        return Task.CompletedTask;
    }

    ...
}

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

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

 

RemoteAuthenticationHandler

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

在上面介紹的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