1. 程式人生 > >ASP.NET Core 執行原理解剖[5]:Authentication

ASP.NET Core 執行原理解剖[5]:Authentication

原文: ASP.NET Core 執行原理解剖[5]:Authentication

在現代應用程式中,認證已不再是簡單的將使用者憑證儲存在瀏覽器中,而要適應多種場景,如App,WebAPI,第三方登入等等。在 ASP.NET 4.x 時代的Windows認證和Forms認證已無法滿足現代化的需求,因此在ASP.NET Core 中對認證及授權進行了全新設計,使其更加靈活,可以應付各種場景。在上一章中,我們提到HttpContext中認證相關的功能放在了獨立的模組中,以擴充套件的方式來展現,以保證HttpContext的簡潔性,本章就來介紹一下 ASP.NET Core 認證系統的整個輪廓,以及它的切入點。

目錄

本系列文章從原始碼分析的角度來探索 ASP.NET Core 的執行原理,分為以下幾個章節:

ASP.NET Core 執行原理解剖[1]:Hosting

ASP.NET Core 執行原理解剖[2]:Hosting補充之配置介紹

ASP.NET Core 執行原理解剖[3]:Middleware-請求管道的構成

ASP.NET Core 執行原理解剖[4]:進入HttpContext的世界

ASP.NET Core 執行原理解剖[5]:Authentication(Current)

  1. AuthenticationHttpContextExtensions
  2. IAuthenticationSchemeProvider
  3. IAuthenticationHandlerProvider
  4. IAuthenticationService
  5. Usage

AuthenticationHttpContextExtensions

AuthenticationHttpContextExtensions 類是對 HttpContext 認證相關的擴充套件,它提供瞭如下擴充套件方法:

public static class AuthenticationHttpContextExtensions
{
    public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
        context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);

    public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }
    public static Task ForbidAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }
    public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) {}
    public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }
    public static Task<string> GetTokenAsync(this HttpContext context, string scheme, string tokenName) { }
}

主要包括如上6個擴充套件方法,其它的只是一些引數過載:

  • SignInAsync 使用者登入成功後頒發一個證書(加密的使用者憑證),用來標識使用者的身份。

  • SignOutAsync 退出登入,如清除Coookie等。

  • AuthenticateAsync 驗證在 SignInAsync 中頒發的證書,並返回一個 AuthenticateResult 物件,表示使用者的身份。

  • ChallengeAsync 返回一個需要認證的標識來提示使用者登入,通常會返回一個 401 狀態碼。

  • ForbidAsync 禁上訪問,表示使用者許可權不足,通常會返回一個 403 狀態碼。

  • GetTokenAsync 用來獲取 AuthenticationProperties 中儲存的額外資訊。

它們的實現都非常簡單,與展示的第一個方法類似,從DI系統中獲取到 IAuthenticationService 介面例項,然後呼叫其同名方法。

因此,如果我們希望使用認證服務,那麼首先要註冊 IAuthenticationService 的例項,ASP.NET Core 中也提供了對應註冊擴充套件方法:

public static class AuthenticationCoreServiceCollectionExtensions
{
    public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
    {
        services.TryAddScoped<IAuthenticationService, AuthenticationService>();
        services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext
        services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
        services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
        return services;
    }

    public static IServiceCollection AddAuthenticationCore(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) 
    {
        services.AddAuthenticationCore();
        services.Configure(configureOptions);
        return services;
    }
}

如上,AddAuthenticationCore 中註冊了認證系統的三大核心物件:IAuthenticationSchemeProviderIAuthenticationHandlerProviderIAuthenticationService,以及一個對Claim進行轉換的 IClaimsTransformation(不常用),下面就來介紹一下這三大物件。

IAuthenticationSchemeProvider

首先來解釋一下 Scheme 是用來做什麼的。因為在 ASP.NET Core 中可以支援各種各樣的認證方式(如,cookie, bearer, oauth, openid 等等),而 Scheme 用來標識使用的是哪種認證方式,不同的認證方式其處理方式是完全不一樣的,所以Scheme是非常重要的。

IAuthenticationSchemeProvider 用來提供對Scheme的註冊和查詢,定義如下:

public interface IAuthenticationSchemeProvider
{
    void AddScheme(AuthenticationScheme scheme);
    Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync();
    Task<AuthenticationScheme> GetSchemeAsync(string name);
    Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync();

    Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync();
    Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync();
    Task<AuthenticationScheme> GetDefaultForbidSchemeAsync();
    Task<AuthenticationScheme> GetDefaultSignInSchemeAsync();
    Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync();
}

AddScheme 方法,用來註冊Scheme,而每一種Scheme最終體現為一個 AuthenticationScheme 型別的物件:

public class AuthenticationScheme
{
    public AuthenticationScheme(string name, string displayName, Type handlerType)
    {
        if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType))
        {
            throw new ArgumentException("handlerType must implement IAuthenticationSchemeHandler.");
        }
        ...
    }

    public string Name { get; }

    public string DisplayName { get; }

    public Type HandlerType { get; }
}

每一個Scheme中還包含一個對應的IAuthenticationHandler型別的Handler,由它來完成具體的處理邏輯,看一下它的預設實現:

public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
{
    private IDictionary<string, AuthenticationScheme> _map = new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal);

    public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
    {
        _options = options.Value;

        foreach (var builder in _options.Schemes)
        {
            var scheme = builder.Build();
            AddScheme(scheme);
        }
    }

    private Task<AuthenticationScheme> GetDefaultSchemeAsync()
        => _options.DefaultScheme != null
        ? GetSchemeAsync(_options.DefaultScheme)
        : Task.FromResult<AuthenticationScheme>(null);
    ....
}

如上,通過一個內部的字典來儲存我們所註冊的Scheme,key為Scheme名稱,然後提供一系列對該字典的查詢。它還提供了一系列的GetDefaultXXXSchemeAsync方法,所使用的Key是通過建構函式中接收的AuthenticationOptions物件來獲取的,如果未配置,則返回為null

對於 AuthenticationOptions 物件,大家可能會比較熟悉,在上面介紹的 AddAuthenticationCore 擴充套件方法中,也是使用該物件來配置認證系統:

public class AuthenticationOptions
{
    private readonly IList<AuthenticationSchemeBuilder> _schemes = new List<AuthenticationSchemeBuilder>();
    public IEnumerable<AuthenticationSchemeBuilder> Schemes => _schemes;
    public IDictionary<string, AuthenticationSchemeBuilder> SchemeMap { get; } = new Dictionary<string, AuthenticationSchemeBuilder>(StringComparer.Ordinal);

    public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder)
    {
        if (SchemeMap.ContainsKey(name))
        {
            throw new InvalidOperationException("Scheme already exists: " + name);
        }
        var builder = new AuthenticationSchemeBuilder(name);
        configureBuilder(builder);
        _schemes.Add(builder);
        SchemeMap[name] = builder;
    }

    public void AddScheme<THandler>(string name, string displayName) where THandler : IAuthenticationHandler
        => AddScheme(name, b =>
        {
            b.DisplayName = displayName;
            b.HandlerType = typeof(THandler);
        });


    public string DefaultScheme { get; set; }
    public string DefaultAuthenticateScheme { get; set; }
    public string DefaultSignInScheme { get; set; }
    public string DefaultSignOutScheme { get; set; }
    public string DefaultChallengeScheme { get; set; }
    public string DefaultForbidScheme { get; set; }
}

該物件可以幫助我們更加方便的註冊Scheme,提供泛型和 AuthenticationSchemeBuilder 兩種方式配置方式。

到此,我們瞭解到,要想使用認證系統,必要先註冊Scheme,而每一個Scheme必須指定一個Handler,否則會丟擲異常,下面我們就來了解一下Handler。

IAuthenticationHandlerProvider

在 ASP.NET Core 的認證系統中,AuthenticationHandler 負責對使用者憑證的驗證,它定義瞭如下介面:

public interface IAuthenticationHandler
{
    Task InitializeAsync(AuthenticationScheme scheme, HttpContext context);
    Task<AuthenticateResult> AuthenticateAsync();
    Task ChallengeAsync(AuthenticationProperties properties);
    Task ForbidAsync(AuthenticationProperties properties);
}

AuthenticationHandler的建立是通過 IAuthenticationHandlerProvider 來完成的:

public interface IAuthenticationHandlerProvider
{
    Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme);
}

Provider 只定義了一個 GetHandlerAsync 方法,來獲取指定的Scheme的Hander,在 ASP.NET Core 中,很多地方都使用了類似的 Provider 模式。

而HandlerProvider的實現,我們通過對上面SchemeProvider的瞭解,應該可以猜到一二,因為在 AuthenticationScheme 中已經包含了Hander:

public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
    public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
    {
        Schemes = schemes;
    }

    public IAuthenticationSchemeProvider Schemes { get; }

    private Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal);

    public async Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme)
    {
        if (_handlerMap.ContainsKey(authenticationScheme))
        {
            return _handlerMap[authenticationScheme];
        }

        var scheme = await Schemes.GetSchemeAsync(authenticationScheme);
        if (scheme == null)
        {
            return null;
        }
        var handler = (context.RequestServices.GetService(scheme.HandlerType) ??
            ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType))
            as IAuthenticationHandler;
        if (handler != null)
        {
            await handler.InitializeAsync(scheme, context);
            _handlerMap[authenticationScheme] = handler;
        }
        return handler;
    }
}

可以看到,AuthenticationHandlerProvider 首先使用 IAuthenticationSchemeProvider 獲取到當前Scheme,然後先從DI中查詢是否有此Scheme中的Handler,如果未註冊到DI系統中,則使用 ActivatorUtilities 來建立其例項,並快取到內部的 _handlerMap 字典中。

IAuthenticationService

IAuthenticationService 本質上是對 IAuthenticationSchemeProvider 和 IAuthenticationHandlerProvider 封裝,用來對外提供一個統一的認證服務介面:

public interface IAuthenticationService
{
    Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme);
    Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties);
    Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties);
    Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
    Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties);
}

這5個方法中,都需要接收一個 scheme 引數,因為只有先指定你要使用的認證方式,才能知道該如何進行認證。

對於上面的前三個方法,我們知道在IAuthenticationHandler中都有對應的實現,而SignInAsyncSignOutAsync則使用了獨立的定義介面:

public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler
{
    Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties);
}

public interface IAuthenticationSignOutHandler : IAuthenticationHandler
{
    Task SignOutAsync(AuthenticationProperties properties);
}

SignInAsync 和 SignOutAsync 之所以使用獨立的介面,是因為在現代架構中,通常會提供一個統一的認證中心,負責證書的頒發及銷燬(登入和登出),而其它服務只用來驗證證書,並用不到SingIn/SingOut。

而 IAuthenticationService 的預設實現 AuthenticationService 中的邏輯就非常簡單了,只是呼叫Handler中的同名方法:

public class AuthenticationService : IAuthenticationService
{
    public IAuthenticationSchemeProvider Schemes { get; }
    public IAuthenticationHandlerProvider Handlers { get; }
    public IClaimsTransformation Transform { get; }

    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);
        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;
    }
}

AuthenticationService中對這5個方法的實現大致相同,首先會在我們傳入的scheme為null時,來獲取我們所註冊的預設scheme,然後獲取呼叫相應Handler的即可。針對 SignInAsyncSignOutAsync 的實現則會判斷Handler是否實現了對應的介面,若未實現則丟擲異常。

不過在這裡還涉及到如下兩個物件:

AuthenticateResult

AuthenticateResult 用來表示認證的結果:

public class AuthenticateResult
{
    public AuthenticationTicket Ticket { get; protected set; }

    public bool Succeeded => Ticket != null;
    public ClaimsPrincipal Principal => Ticket?.Principal;
    public AuthenticationProperties Properties => Ticket?.Properties;
    public Exception Failure { get; protected set; }
    public bool None { get; protected set; }
    public static AuthenticateResult Success(AuthenticationTicket ticket) => new AuthenticateResult() { Ticket = ticket };
    public static AuthenticateResult NoResult() => new AuthenticateResult() { None = true };
    public static AuthenticateResult Fail(Exception failure) => new AuthenticateResult() { Failure = failure };
    public static AuthenticateResult Fail(string failureMessage) => new AuthenticateResult() { Failure = new Exception(failureMessage) };
}

它主要包含一個核心屬性 AuthenticationTicket

public class AuthenticationTicket
{ 
    public string AuthenticationScheme { get; private set; }
    public ClaimsPrincipal Principal { get; private set; }
    public AuthenticationProperties Properties { get; private set; }
}

我們可以把AuthenticationTicket看成是一個經過認證後頒發的證書,

ClaimsPrincipal 屬性我們較為熟悉,表示證書的主體,在基於宣告的認證中,用來標識一個人的身份(如:姓名,郵箱等等),後續會詳細介紹一下基於宣告的認證。

AuthenticationProperties 屬性用來表示證書頒發的相關資訊,如頒發時間,過期時間,重定向地址等等:

public class AuthenticationProperties
{
    public IDictionary<string, string> Items { get; }

    public string RedirectUri
    {
        get
        {
            string value;
            return Items.TryGetValue(RedirectUriKey, out value) ? value : null;
        }
        set
        {
            if (value != null) Items[RedirectUriKey] = value;
            else
            {
                if (Items.ContainsKey(RedirectUriKey)) Items.Remove(RedirectUriKey);
            }
        }
    }

    ...
}

在上面最開始介紹的HttpContext中的 GetTokenAsync 擴充套件方法便是對AuthenticationProperties的擴充套件:

public static class AuthenticationTokenExtensions
{
    private static string TokenNamesKey = ".TokenNames";
    private static string TokenKeyPrefix = ".Token.";

    public static void StoreTokens(this AuthenticationProperties properties, IEnumerable<AuthenticationToken> tokens) {}
    public static bool UpdateTokenValue(this AuthenticationProperties properties, string tokenName, string tokenValue) {}
    public static IEnumerable<AuthenticationToken> GetTokens(this AuthenticationProperties properties) { }

    public static string GetTokenValue(this AuthenticationProperties properties, string tokenName)
    {
        var tokenKey = TokenKeyPrefix + tokenName;
        return properties.Items.ContainsKey(tokenKey) ? properties.Items[tokenKey] : null;
    }

    public static Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName) 
        => auth.GetTokenAsync(context, scheme: null, tokenName: tokenName);

    public static async Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string scheme, string tokenName)
    {
        var result = await auth.AuthenticateAsync(context, scheme);
        return result?.Properties?.GetTokenValue(tokenName);
    }
}

如上,Token擴充套件只是對AuthenticationProperties中的 Items 屬性進行新增和讀取。

IClaimsTransformation

IClaimsTransformation 用來對由我們的應用程式傳入的 ClaimsPrincipal 進行轉換,它只定義了一個 Transform 方法:

public interface IClaimsTransformation
{
    Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal);
}

其預設實現,不做任何處理,直接返回。它適合於全域性的為 ClaimsPrincipal 新增一些預定義的宣告,如添加當前時間等,然後在DI中把我們的實現註冊進去即可。

Usage

下面我們演示一下 ASP.NET Core 認證系統的實際用法:

首先,我們要定義一個Handler:

public class MyHandler : IAuthenticationHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler
{
    public AuthenticationScheme Scheme { get; private set; }
    protected HttpContext Context { get; private set; }

    public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
    {
        Scheme = scheme;
        Context = context;
        return Task.CompletedTask;
    }

    public async Task<AuthenticateResult> AuthenticateAsync()
    {
        var cookie = Context.Request.Cookies["mycookie"];
        if (string.IsNullOrEmpty(cookie))
        {
            return AuthenticateResult.NoResult();
        }
        return AuthenticateResult.Success(Deserialize(cookie));
    }

    public Task ChallengeAsync(AuthenticationProperties properties)
    {
        Context.Response.Redirect("/login");
        return Task.CompletedTask;
    }

    public Task ForbidAsync(AuthenticationProperties properties)
    {
        Context.Response.StatusCode = 403;
        return Task.CompletedTask;
    }

    public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
    {
        var ticket = new AuthenticationTicket(user, properties, Scheme.Name);
        Context.Response.Cookies.Append("myCookie", Serialize(ticket));
        return Task.CompletedTask;
    }

    public Task SignOutAsync(AuthenticationProperties properties)
    {
        Context.Response.Cookies.Delete("myCookie");
        return Task.CompletedTask;
    }
}

如上,在 SignInAsync 中將使用者的Claim序列化後儲存到Cookie中,在 AuthenticateAsync 中從Cookie中讀取並反序列化成使用者Claim。

然後在DI系統中註冊我們的Handler和Scheme:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthenticationCore(options => options.AddScheme<MyHandler>("myScheme", "demo scheme"));
}

最後,便可以通過HttpContext來呼叫認證系統了:

public void Configure(IApplicationBuilder app)
{
    // 登入
    app.Map("/login", builder => builder.Use(next =>
    {
        return async (context) =>
        {
            var claimIdentity = new ClaimsIdentity();
            claimIdentity.AddClaim(new Claim(ClaimTypes.Name, "jim"));
            await context.SignInAsync("myScheme", new ClaimsPrincipal(claimIdentity));
        };
    }));

    // 退出
    app.Map("/logout", builder => builder.Use(next =>
    {
        return async (context) =>
        {
            await context.SignOutAsync("myScheme");
        };
    }));

    // 認證
    app.Use(next =>
    {
        return async (context) =>
        {
            var result = await context.AuthenticateAsync("myScheme");
            if (result?.Principal != null) context.User = result.Principal;
            await next(context);
        };
    });

    // 授權
    app.Use(async (context, next) =>
    {
        var user = context.User;
        if (user?.Identity?.IsAuthenticated ?? false)
        {
            if (user.Identity.Name != "jim") await context.ForbidAsync("myScheme");
            else await next();
        }
        else
        {
            await context.ChallengeAsync("myScheme");
        }
    });

    // 訪問受保護資源
    app.Map("/resource", builder => builder.Run(async (context) => await context.Response.WriteAsync("Hello, ASP.NET Core!")));
}

在這裡完整演示了 ASP.NET Core 認證系統的基本用法,當然,在實際使用中要比這更加複雜,如安全性,易用性等方面的完善,但本質上也就這麼多東西。

總結

本章基於 HttpAbstractions 對 ASP.NET Core 認證系統做了一個簡單的介紹,但大多是一些抽象層次的定義,並未涉及到具體的實現。因為現實中有各種各樣的場景無法預測,HttpAbstractions 提供了統一的認證規範,在我們的應用程式中,可以根據具體需求來靈活的擴充套件適合的認證方式。不過在 Security 提供了更加具體的實現方式,也包含了 Cookie, JwtBearer, OAuth, OpenIdConnect 等較為常用的認證實現。在下個系列會來詳細介紹一下 ASP.NET Core 的認證與授權,更加偏向於實戰,敬請期待!

ASP.NET Core 在GitHub上的開源地址為:https://github.com/aspnet,包含了100多個專案,ASP.NET Core 的核心是 HttpAbstractions ,其它的都是圍繞著 HttpAbstractions 進行的擴充套件。本系列文章所涉及到的原始碼只包含 HostingHttpAbstractions ,它們兩個已經構成了一個完整的 ASP.NET Core 執行時,不需要其它模組,就可以輕鬆應對一些簡單的場景。當然,更多的時候我們還會使用比較熟悉的 Mvc 來大大提高開發速度和體驗,後續再來介紹一下MVC的執行方式。