理解 ASP.NET Core: 認證

ASP.NET Core 認證

一般在應用程序中,安全分爲先後兩個步驟:驗證和受權。驗證負責檢查當前請求者的身份,而受權則根據上一步獲得的身份決定當前請求者是否可以訪問指望的資源。git

既然安全從驗證開始,咱們也就從驗證開始介紹安全。github

驗證的核心概念

咱們先從比較簡單的場景開始考慮,例如在 Web API 開發中,須要驗證請求方是否提供了安全令牌,安全令牌是否有效。若是無效,那麼 API 端應該拒絕提供服務。在命名空間 Microsoft.AspNetCore.Authentication 下,定義關於驗證的核心接口。對應的程序集是 Microsoft.AspNetCore.Authentication.Abstractions.dll。安全

驗證接口 IAuthenticationHandler

在 ASP.NET 下,驗證中包含 3 個基本操做:架構

Authenticate 驗證

驗證操做負責基於當前請求的上下文,使用來自請求中的信息,例如請求頭、Cookie 等等來構造用戶標識。構建的結果是一個 AuthenticateResult 對象,它指示了驗證是否成功,若是成功的話,用戶標識將能夠在驗證票據中找到。async

常見的驗證包括:ide

  • 基於 Cookie 的驗證,從請求的 Cookie 中驗證用戶
  • 基於 JWT Bearer 的驗證,從請求頭中提取 JWT 令牌進行驗證

Challenge 質詢

在受權管理階段,若是用戶沒有獲得驗證,但所指望訪問的資源要求必須獲得驗證的時候,受權服務會發出質詢。例如,當匿名用戶訪問受限資源的時候,或者當用戶點擊登陸連接的時候。受權服務會經過質詢來相應用戶。函數

例如網站

  • 基於 Cookie 的驗證會將用戶重定向到登陸頁面
  • 基於 JWT 的驗證會返回一個帶有 www-authenticate: bearer 響應頭的 401 響應來提醒客戶端須要提供訪問憑據

質詢操做應該讓用戶知道應該使用何種驗證機制來訪問請求的資源。ui

Forbid 拒絕

在受權管理階段,若是用戶已經經過了驗證,可是對於其訪問的資源並無獲得許可,此時會使用拒絕操做。this

例如:

  • Cookie 驗證模式下,已經登陸可是沒有訪問權限的用戶,被重定向到一個提示無權訪問的頁面
  • JWT 驗證模式下,返回 403
  • 在自定義驗證模式下,將沒有權限的用戶重定向到申請資源的頁面

拒絕訪問處理應該讓用戶知道:

  • 它已經經過了驗證
  • 可是沒有權限訪問請求的資源

在這個場景下,能夠看到,驗證須要提供的基本功能就包括了驗證和驗證失敗後的拒絕服務兩個操做。在 ASP.NET Core 中,驗證被稱爲 Authenticate,拒絕被稱爲 Forbid。 在供消費者訪問的網站上,若是咱們但願在驗證失敗後,不是像 API 同樣直接返回一個錯誤頁面,而是將用戶導航到登陸頁面,那麼,就還須要增長一個操做,這個操做的本質是但願用戶再次提供安全憑據,在 ASP.NET Core 中,這個操做被稱爲 Challenge。這 3 個操做結合在一塊兒,就是驗證最基本的要求,以接口形式表示,就是 IAuthenticationHandler 接口,以下所示:

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

驗證的結果是一個 AuthenticateResult 對象。值得注意的是,它還提供了一個靜態方法 NoResult() 用來返回沒有獲得結果,靜態方法 Fail() 生成一個表示驗證異常的結果,而 Success() 成功則須要提供驗證票據。

經過驗證以後,會返回一個包含了請求者票據的驗證結果。

namespace Microsoft.AspNetCore.Authentication
{
    public class AuthenticateResult
    {
        // ......
        public static AuthenticateResult NoResult()
        {
            return new AuthenticateResult() { None = true };
        }
        public static AuthenticateResult Fail(Exception failure)
        {
            return new AuthenticateResult() { Failure = failure };
        }
        public static AuthenticateResult Success(AuthenticationTicket ticket)
        {
            if (ticket == null)
            {
                throw new ArgumentNullException(nameof(ticket));
            }
            return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties };
        }
        public static AuthenticateResult Success(AuthenticationTicket ticket)
        {
            if (ticket == null)
            {
                throw new ArgumentNullException(nameof(ticket));
            }
            return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties };
        }
        // ......
    }
}

在 GitHub 中查看 AuthenticateResult 源碼

那麼驗證的信息來自哪裏呢?除了前面介紹的 3 個操做以外,還要求一個初始化的操做 Initialize,經過這個方法來提供當前請求的上下文信息。

在 GitHub 中查看 IAuthenticationHandler 定義

支持登陸和登出操做的驗證接口

有的時候,咱們還但願提供登出操做,增長登出操做的接口被稱爲 IAuthenticationSignOutHandler。

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

在 GitHub 中查看 IAuthenticationSignOutHandler 源碼

在登出的基礎上,若是還但願提供登陸操做,那麼就是 IAuthenticationSignInHandler 接口。

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

在 GitHub 中查看 IAuthenticationSignInHandler 源碼

實現驗證支持的抽象基類 AuthenticationHandler

直接實現接口仍是比較麻煩的,在命名空間 Microsoft.AspNetCore.Authentication 下,微軟提供了抽象基類 AuthenticationHandler 以方便驗證控制器的開發,其它控制器能夠從該控制器派生,以取得其提供的服務。

namespace Microsoft.AspNetCore.Authentication
{
    public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new()
    {
         protected AuthenticationHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        {
            Logger = logger.CreateLogger(this.GetType().FullName);
            UrlEncoder = encoder;
            Clock = clock;
            OptionsMonitor = options;
        }
    }
    // ......
}

經過類的定義能夠看到,它使用了泛型。每一個控制器應該有一個對應該控制器的配置選項,經過泛型來指定驗證處理器所使用的配置類型,在構造函數中,能夠看到它被用於獲取對應的配置選項對象。

在 GitHub 中查看 AuthenticationHandler 源碼

經過 InitializeAsync(),驗證處理器能夠得到當前請求的上下文對象 HttpContext。

public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)

最終,做爲抽象類的 ,但願派生類來完成這個驗證任務,抽象方法 HandleAuthenticateAsync() 提供了擴展點。

/// <summary>
/// Allows derived types to handle authentication.
/// </summary>
/// <returns>The <see cref="AuthenticateResult"/>.</returns>
protected abstract Task<AuthenticateResult> HandleAuthenticateAsync();

驗證的結果是一個 AuthenticateResult。

而拒絕服務則簡單的多,直接在這個抽象基類中提供了默認實現。直接返回 HTTP 403。

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

剩下的一個也同樣,提供了默認實現。直接返回 HTTP 401 響應。

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

Jwt 驗證處理器是如何實現的?

對於 JWT 來講,並不涉及到登入和登出,因此它須要從實現 IAuthenticationHandler 接口的抽象基類 AuthenticationHandler 派生出來便可。從 AuthenticationHandler 派生出來的 JwtBearerHandler 實現基於本身的配置選項 JwtBearerOptions。因此該類定義就變得以下所示,而構造函數顯然配合了抽象基類的要求。

namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
    public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>
    {
        public JwtBearerHandler(
            IOptionsMonitor<JwtBearerOptions> options, 
            ILoggerFactory logger, 
            UrlEncoder encoder, 
            ISystemClock clock)
            : base(options, logger, encoder, clock)
        { }
        // ......
    }
}

在 GitHub 中查看 JwtBearerHandler 源碼

真正的驗證則在 HandleAuthenticateAsync() 中實現。下面的代碼是否是就很熟悉了,從請求頭中獲取附帶的 JWT 訪問令牌,而後驗證該令牌的有效性,核心代碼以下所示。

string authorization = Request.Headers[HeaderNames.Authorization];

// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
    return AuthenticateResult.NoResult();
}

if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
    token = authorization.Substring("Bearer ".Length).Trim();
}

// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
    return AuthenticateResult.NoResult();
}

// ......
principal = validator.ValidateToken(token, validationParameters, out validatedToken);

在 GitHub 中查看 JwtBearerHandler 源碼

註冊 Jwt 驗證處理器

在 ASP.NET Core 中,你可使用各類驗證處理器,並不只僅只能使用一個,驗證控制器須要一個名稱,它被看做該驗證模式 Schema 的名稱。Jwt 驗證模式的默認名稱就是 "Bearer",經過字符串常量 JwtBearerDefaults.AuthenticationScheme 定義。

namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
    /// <summary>
    /// Default values used by bearer authentication.
    /// </summary>
    public static class JwtBearerDefaults
    {
        /// <summary>
        /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions
        /// </summary>
        public const string AuthenticationScheme = "Bearer";
    }
}

在 GitHub 中查看 JwtBearerDefaults 源碼

最終經過 AuthenticationBuilder 的擴展方法 AddJwtBearer() 將 Jwt 驗證控制器註冊到依賴注入的容器中。

public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder)
            => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { });

public static AuthenticationBuilder AddJwtBearer(
    this AuthenticationBuilder builder, 
    string authenticationScheme, 
    string displayName, 
    Action<JwtBearerOptions> configureOptions)
{
            builder.Services.TryAddEnumerable(
                ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, 
                JwtBearerPostConfigureOptions>());
            return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(
                authenticationScheme, displayName, configureOptions);
}

在 GitHub 中查看 JwtBearerExtensions 擴展方法源碼

驗證架構 Schema

一種驗證處理器,加上對應的驗證配置選項,咱們再爲它起一個名字,組合起來就成爲一種驗證架構 Schema。在 ASP.NET Core 中,能夠註冊多種驗證架構。例如,受權策略可使用架構的名稱來指定所使用的驗證架構來使用特定的驗證方式。在配置驗證的時候,一般設置默認的驗證架構。當沒有指定驗證架構的時候,就會使用默認架構進行處理。

還能夠

  • 對於 authenticate, challenge, 以及 forbid 操做使用不一樣的驗證架構
  • 使用策略來組合多種驗證架構

註冊的驗證模式,最終變成 AuthenticationScheme,註冊到依賴注入服務中。

public class AuthenticationScheme
{
    public string Name { get; }
    public string? DisplayName { get; }
    
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
    public Type HandlerType { get; }
}

在 GitHub 中查看 AuthenticationScheme 源碼

使用驗證處理器

IAuthenticationSchemeProvider

各類驗證架構被保存到一個 IAuthenticationSchemeProvider 中。

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

在 GitHub 中查看 IAuthenticationSchemeProvider 源碼

IAuthenticationHandlerProvider

最終的使用是經過 IAuthenticationHandlerProvider 來實現的,經過一個驗證模式的字符串名稱,能夠取得所對應的驗證控制器。

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

在 GitHub 中查看 IAuthenticationHandlerProvider 源碼

它的默認實現是 AuthenticationHandlerProvider,源碼並不複雜。

public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
    public IAuthenticationSchemeProvider Schemes { get; }
    private readonly Dictionary<string, IAuthenticationHandler> _handlerMap 
        = new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal);
    
    public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
    {
        Schemes = schemes;
    }
    
    public async Task<IAuthenticationHandler?> GetHandlerAsync(HttpContext context, string authenticationScheme)
    {
        if (_handlerMap.TryGetValue(authenticationScheme, out var value))
        {
            return value;
        }

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

在 GitHub 中查看 AuthenticationHandlerProvider 源碼

Authentication 中間件 AuthenticationMiddleware

驗證中間件的處理就沒有那麼複雜了。

找到默認的驗證模式,使用默認驗證模式的名稱取得對應的驗證處理器,若是驗證成功的話,把當前請求用戶的主體放到當前請求上下文的 User 上。

裏面還有一段特別的代碼,用來找出哪些驗證處理器實現了 IAuthenticationHandlerProvider,並依次調用它們,看看是否須要提取終止請求處理過程。

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Authentication
{
    public class AuthenticationMiddleware
    {
        private readonly RequestDelegate _next;

        public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
        {
            if (next == null)
            {
                throw new ArgumentNullException(nameof(next));
            }
            if (schemes == null)
            {
                throw new ArgumentNullException(nameof(schemes));
            }

            _next = next;
            Schemes = schemes;
        }

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

            // Give any IAuthenticationRequestHandler schemes a chance to handle the request
            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);
        }
    }
}

在 GitHub 中查看 AuthenticationMiddle 源碼

參考資料

相關文章
相關標籤/搜索