理解ASP.NET Core - 授權(Authorization)

注:本文隸屬於《理解ASP.NET Core》系列文章,請查看置頂博客或點擊此處查看全文目錄

之前,我們已經瞭解了ASP.NET Core中的身份認證,現在,我們來聊一下授權。

老規矩,示例程序源碼XXTk.Auth.Samples已經提交了,需要的請自取。

概述

ASP.NET Core中的授權方式有很多,我們一起了解一下其中三種較爲常見的方式:

  • 基於角色的授權
  • 基於聲明的授權
  • 基於策略的授權

其中,基於策略的授權是我們要了解的重點。

在進入正文之前,我們要先認識一個很重要的特性——AuthorizeAttribute,通過它,我們可以很方便的針對Controller、Action等維度進行權限控制:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeAttribute : Attribute, IAuthorizeData
{
    public AuthorizeAttribute() { }

    public AuthorizeAttribute(string policy)
    {
        Policy = policy;
    }

    // 策略
    public string? Policy { get; set; }

    // 角色,可以通過英文逗號將多個角色分隔開,從而形成一個列表
    public string? Roles { get; set; }

    // 身份認證方案,可以通過英文逗號將多個身份認證方案分隔開,從而形成一個列表
    public string? AuthenticationSchemes { get; set; }
}

另外,爲了方便測試,我們先添加一下基於Cookie的身份認證:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.Cookie.Name = "auth";
                // 用戶未登錄時返回401
                options.Events.OnRedirectToLogin = context =>
                {
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    return Task.CompletedTask;
                };
                // 用戶無權限訪問時返回403
                options.Events.OnRedirectToAccessDenied = context =>
                {
                    context.Response.StatusCode = StatusCodes.Status403Forbidden;
                    return Task.CompletedTask;
                };
            });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

Configure中,通過app.UseAuthorization()將授權中間件AuthorizationMiddleware添加到了請求管道。

基於角色的授權

實例程序請參考:XXTk.Auth.Samples.RoleBased.HttpApi

顧名思義,基於角色的授權就是檢查用戶是否擁有指定角色,如果是則授權通過,否則不通過。

我們先看一個簡單的例子:

[Authorize(Roles = "Admin")]
public string GetForAdmin()
{
    return "Admin only";
}

這裏,我們將AuthorizeAttribute特性的Roles屬性設置爲了Admin,也就是說,如果用戶想要訪問GetForAdmin接口,則必須擁有角色Admin。

如果某個接口想要允許多個角色訪問,該怎麼做呢?很簡單,通過英文逗號(,)分隔多個角色即可:

[Authorize(Roles = "Developer,Tester")]
public string GetForDeveloperOrTester()
{
    return "Developer || Tester";
}

就像上面這樣,通過逗號將DeveloperTester分隔開來,當接到請求時,若用戶擁有角色Developer和Tester其一,就允許訪問該接口。

最後,如果某個接口要求用戶必須同時擁有多個角色時才允許訪問,那我們可以通過添加多個AuthorizeAttribute特性來達到目的:

[Authorize(Roles = "Developer")]
[Authorize(Roles = "Tester")]
public string GetForDeveloperAndTester()
{
    return "Developer && Tester";
}

只有當用戶同時擁有角色DeveloperTester時,才允許訪問該接口。

你現在可能已經迫不及待要親自驗證一下了,不過你還記得如何設置用戶的角色嗎?我們在身份認證的文章中介紹過,在頒發身份票據時,可以通過聲明添加角色,例如:

public async Task<IActionResult> LoginForAdmin()
{
    var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
    identity.AddClaims(new[]
    {
        new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString("N")),
        new Claim(ClaimTypes.Name, "AdminOnly"),
        // 添加角色Admin
        new Claim(ClaimTypes.Role, "Admin")
    });

    var principal = new ClaimsPrincipal(identity);

    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);

    return Ok();
}

由於篇幅限制,其他的登錄代碼就不貼了,可以在示例程序中找到。

基於聲明的授權

實例程序請參考:XXTk.Auth.Samples.ClaimsBased.HttpApi

上面介紹的基於角色的授權,實際上就是基於聲明中的“角色”來實現的,而基於聲明的授權,則將範圍擴展到了所有聲明(而不僅僅是角色)。

基於聲明的授權,是在基於策略的授權基礎上實現的。爲什麼這麼說呢?因爲我們需要通過添加策略來使用聲明:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthorization(options =>
        {
            // ... 可以在此處添加策略
        });
    }
}

一個簡單的聲明策略如下:

options.AddPolicy("RankClaim", policy => policy.RequireClaim("Rank"));

該策略名稱爲RankClaim,要求用戶具有聲明Rank,具體Rank對應的值是多少,不關心,只要有這個聲明就好了。

當然,我們也可以將Rank的值限定一下:

options.AddPolicy("RankClaimP3", policy => policy.RequireClaim("Rank", "P3"));
options.AddPolicy("RankClaimM3", policy => policy.RequireClaim("Rank", "M3"));

我們添加了兩條策略:RankClaimP3RankClaimM3,除了要求用戶具有聲明Rank外,還分別要求Rank的值爲P3M3

類似於基於角色的聲明,我們也可以添加“Or”、“And”邏輯的策略:

options.AddPolicy("RankClaimP3OrM3", policy => policy.RequireClaim("Rank", "P3", "M3"));
options.AddPolicy("RankClaimP3AndM3", policy => policy.RequireClaim("Rank", "P3").RequireClaim("Rank", "M3"));

策略RankClaimP3OrM3要求用戶具有聲明Rank,且值爲P3M3即可;而策略RankClaimP3AndM3要求用戶具有聲明Rank,且值必須同時包含P3M3

策略的用法與之前的類似(注意策略不能像角色一樣通過逗號分隔):

// 僅要求用戶具有聲明“Rank”,不關心值是多少
[Authorize(Policy = "RankClaim")]
public string GetForRankClaim()
{
    return "Rank claim only";
}

// 要求用戶具有聲明“Rank”,且值爲“M3”
[HttpGet("GetForRankClaimP3")]
[Authorize(Policy = "RankClaimP3")]
public string GetForRankClaimP3()
{
    return "Rank claim P3";
}

// 要求用戶具有聲明“Rank”,且值爲“P3” 或 “M3”
[Authorize(Policy = "RankClaimP3OrM3")]
public string GetForRankClaimP3OrM3()
{
    return "Rank claim P3 || M3";
}

表示“And”邏輯的策略可以有兩種寫法:

// 要求用戶具有聲明“Rank”,且值爲“P3” 和 “M3”
[Authorize(Policy = "RankClaimP3AndM3")]
public string GetForRankClaimP3AndM3V1()
{
    return "Rank claim P3 && M3";
}

// 要求用戶具有聲明“Rank”,且值爲“P3” 和 “M3”
[Authorize(Policy = "RankClaimP3")]
[Authorize(Policy = "RankClaimM3")]
public string GetForRankClaimP3AndM3V2()
{
    return "Rank claim P3 && M3";
}

另外,有時候聲明策略略微有些複雜,可以使用RequireAssertion來實現:

options.AddPolicy("ComplexClaim", policy => policy.RequireAssertion(context =>
    context.User.HasClaim(c => (c.Type == "Rank" || c.Type == "Name") && c.Issuer == "Issuer")));

基於策略的授權

實例程序請參考:XXTk.Auth.Samples.PolicyBased.HttpApi

通常來說,以上兩種授權方式僅適用於較爲簡單的業務場景,而當業務場景比較複雜時,它倆就顯得無能爲力了。因此,我們必須能夠設計更加自由的策略,也就是基於策略的授權。

基於策略的授權,我打算將其分成兩種類型來介紹:簡單策略和動態策略。

簡單策略

在上面,我們制定策略時,使用了大量的RequireXXX,我們也希望能夠將自定義策略封裝一下,當然,你可以寫一些擴展方法,不過我更加推薦使用IAuthorizationRequirementIAuthorizationHandler

現在,我們虛構一個場景:網吧管理,未滿18歲的人員不準入內,只允許年滿18歲的成年人進入。爲此,我們需要一個限定最小年齡的要求:

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public MinimumAgeRequirement(int minimumAge) =>
       MinimumAge = minimumAge;

    public int MinimumAge { get; }
}

現在,要求有了,我們還需要一個授權處理器,來校驗用戶是否真的達到了指定年齡:

public class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        // 這裏生日信息可以從其他地方獲取,如數據庫,不限於聲明
        var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var today = DateTime.Today;
        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        int calculatedAge = today.Year - dateOfBirth.Year;
        if (dateOfBirth > today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        // 若年齡達到最小年齡要求,則授權通過
        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

當校驗通過時,調用context.Succeed來指示授權通過。當校驗不通過時,我們有兩種處理方式:

  • 一種是直接返回Task.CompletedTask,這將允許後續的Handler繼續進行校驗,這些Handler中任意一個認證通過,都視爲該用戶授權通過。
  • 另一種是通過調用context.Fail來指示授權不通過,並且後續的Handler仍會執行(即使後續的Handler有授權通過的,也視爲授權不通過)。如果你想在調用context.Fail後,立即返回而不再執行後續的Handler,可以將選項AuthorizationOptions的屬性InvokeHandlersAfterFailure設置爲false來達到目的,默認爲true

現在,我們給虛構的場景增加一個授權邏輯:當用戶未滿18歲,但是其角色爲網吧老闆時,也允許其入內。

爲了實現這個邏輯,我們再增加一個授權處理器:

public class MinimumAgeAnotherAuthorizationHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var isBoss = context.User.IsInRole("InternetBarBoss");

        if (isBoss)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

授權要求和授權處理器我們都已經實現了,接下來就是添加策略了,不過在這之前,不要忘了注入我們的要求和授權處理器:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.TryAddEnumerable(ServiceDescriptor.Transient<IAuthorizationHandler, MinimumAgeAuthorizationHandler>());
        services.TryAddEnumerable(ServiceDescriptor.Transient<IAuthorizationHandler, MinimumAgeAnotherAuthorizationHandler>());
        
        services.AddAuthorization(options =>
        {
            options.AddPolicy("AtLeast18Age", policy => policy.Requirements.Add(new MinimumAgeRequirement(18)));
        });
    }
}

需要注意的是,我們可以將Handler註冊爲任意的生命週期,不過,當Handler中依賴其他服務時,一定要注意生命週期提升的問題。

我們添加了一個名爲AtLeast18Age的策略,該策略創建了一個MinimumAgeRequirement實例,要求最低年齡爲18歲,並將其添加到了policyRequirements屬性中。

你可以寫一個類似的接口進行測試:

[Authorize(Policy = "AtLeast18Age")]
public string GetForAtLeast18Age()
{
    return "At least 18 age";
}

最後,多說一句,如果你想讓一個Handler可以同時處理多個Requirement,可以這樣做:

public class MultiRequirementsAuthorizationHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements;

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is Custom1Requirement)
            {
                // ... 一些校驗

                context.Succeed(requirement);
            }
            else if (requirement is Custom2Requirement)
            {
                // ... 一些校驗

                context.Succeed(requirement);
            }
        }

        return Task.CompletedTask;
    }
}

public class Custom1Requirement : IAuthorizationRequirement
{
}

public class Custom2Requirement : IAuthorizationRequirement
{
}

動態策略

現在,問題又來了,如果我們的場景有多種年齡限制,比如有的要求18歲,有的要求20,還有的只要求10歲,我們總不能一個個的把這些策略都提前創建好吧,要搞死人...如果能夠動態地創建策略就好了!

下面我們嘗試動態地創建多種最小年齡策略:

首先,繼承AuthorizeAttribute來實現一個自定義授權特性MinimumAgeAuthorizeAttribute

public class MinimumAgeAuthorizeAttribute : AuthorizeAttribute
{
    // 策略名前綴
    public const string PolicyPrefix = "MinimumAge";

    // 通過構造函數傳入最小年齡
    public MinimumAgeAuthorizeAttribute(int minimumAge) =>
        MinimumAge = minimumAge;

    public int MinimumAge
    {
        get
        {
            // 從策略名中解析出最小年齡
            if (int.TryParse(Policy[PolicyPrefix.Length..], out var age))
            {
                return age;
            }

            return default;
        }
        set
        {
            // 生成動態的策略名,如 MinimumAge18 表示最小年齡爲18的策略
            Policy = $"{PolicyPrefix}{value}";
        }
    }
}

邏輯很簡單,就是將策略名前綴+傳入的最小年齡參數動態地拼接爲一個策略名,並且還可以通過策略名反向解析出最小年齡。

好了,現在策略名可以動態創建了,那下一步就是根據策略名動態創建出策略實例了,可以通過替換接口IAuthorizationPolicyProvider的默認實現來達到目的:

public class AppAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
    // 引用自第三方庫 Nito.AsyncEx
    private static readonly AsyncLock _mutex = new();
    private readonly AuthorizationOptions _authorizationOptions;

    public AppAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
    {
        BackupPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
        _authorizationOptions = options.Value;
    }
    
    // 若不需要自定義實現,則均使用默認的
    private DefaultAuthorizationPolicyProvider BackupPolicyProvider { get; }

    public async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        if(policyName is null) throw new ArgumentNullException(nameof(policyName));

        // 若策略實例已存在,則直接返回
        var policy = await BackupPolicyProvider.GetPolicyAsync(policyName);
        if(policy is not null)
        {
            return policy;
        }
        
        using (await _mutex.LockAsync())
        {
            var policy = await BackupPolicyProvider.GetPolicyAsync(policyName);
            if(policy is not null)
            {
                return policy;
            }
            
            if (policyName.StartsWith(MinimumAgeAuthorizeAttribute.PolicyPrefix, StringComparison.OrdinalIgnoreCase) 
                && int.TryParse(policyName[MinimumAgeAuthorizeAttribute.PolicyPrefix.Length..], out var age))
            {
                // 動態創建策略
                var builder = new AuthorizationPolicyBuilder();
                // 添加 Requirement
                builder.AddRequirements(new MinimumAgeRequirement(age));
                policy = builder.Build();
                // 將策略添加到選項
                _authorizationOptions.AddPolicy(policyName, policy);
    
                return policy;
            }
        }

        return null;
    }

    public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
    {
        return BackupPolicyProvider.GetDefaultPolicyAsync();
    }

    public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
    {
        return BackupPolicyProvider.GetFallbackPolicyAsync();
    }
}

最後,只需要注入一下服務就好啦:

services.AddTransient<IAuthorizationPolicyProvider, AppAuthorizationPolicyProvider>();

現在你就可以使用MinimumAgeAuthorizeAttribute進行授權了,比如限制最小年齡20歲:

[MinimumAgeAuthorize(20)]
public string GetForAtLeast20Age()
{
    return "At least 20 age";
}

設計原理

現在,基礎用法我們已經瞭解了,接下來就一起學習一下它背後的原理吧。

鑑於涉及到的源碼較多,所以爲了控制文章長度,下面只列舉核心代碼。

首先,我們再熟悉一下AuthorizeAttribute

public interface IAuthorizeData
{
    // 策略
    string? Policy { get; set; }

    // 角色,可以通過英文逗號將多個角色分隔開,從而形成一個列表
    string? Roles { get; set; }

    // 身份認證方案,可以通過英文逗號將多個身份認證方案分隔開,從而形成一個列表
    string? AuthenticationSchemes { get; set; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeAttribute : Attribute, IAuthorizeData
{
    public AuthorizeAttribute() { }

    public AuthorizeAttribute(string policy)
    {
        Policy = policy;
    }
    
    public string? Policy { get; set; }
    public string? Roles { get; set; }
    public string? AuthenticationSchemes { get; set; }
}

Attribute自然不必多說,我們要注意的是AuthorizeAttribute實現的接口爲IAuthorizeData

接下來我們從services.AddAuthorization入手,看看針對授權都註冊了哪些服務:

你可能會疑問,即使我沒有顯式的添加services.AddAuthorization這行代碼,程序也不會報錯,其實這個我們在前文 Startup 中就提到過,services.AddControllers()中會默認調用AddAuthorization

public static IServiceCollection AddAuthorization(this IServiceCollection services)
{
    services.AddAuthorizationCore();
    services.AddAuthorizationPolicyEvaluator();
    return services;
}

public static IServiceCollection AddAuthorizationCore(this IServiceCollection services)
{
    services.AddOptions();

    services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationService>());
    services.TryAdd(ServiceDescriptor.Transient<IAuthorizationPolicyProvider, DefaultAuthorizationPolicyProvider>());
    services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerProvider, DefaultAuthorizationHandlerProvider>());
    services.TryAdd(ServiceDescriptor.Transient<IAuthorizationEvaluator, DefaultAuthorizationEvaluator>());
    services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerContextFactory, DefaultAuthorizationHandlerContextFactory>());
    services.TryAddEnumerable(ServiceDescriptor.Transient<IAuthorizationHandler, PassThroughAuthorizationHandler>());
    return services;
}

public static IServiceCollection AddAuthorizationPolicyEvaluator(this IServiceCollection services)
{
    services.TryAddSingleton<AuthorizationPolicyMarkerService>();
    services.TryAddTransient<IPolicyEvaluator, PolicyEvaluator>();
    services.TryAddTransient<IAuthorizationMiddlewareResultHandler, AuthorizationMiddlewareResultHandler>();
    return services;
}

我們整理下這裏註冊了哪些接口:

  • IAuthorizationService
  • IAuthorizationPolicyProvider
  • IAuthorizationHandlerProvider
  • IAuthorizationEvaluator
  • IAuthorizationHandlerContextFactory
  • IAuthorizationHandler
  • AuthorizationPolicyMarkerService
  • IPolicyEvaluator
  • IAuthorizationMiddlewareResultHandler

這裏面有幾個接口是我們之前見過的,比如IAuthorizationPolicyProviderIAuthorizationHandler。不着急研究其他幾個接口的作用,咱們接着看下AuthorizationOptions

public class AuthorizationOptions
{
    // 存放添加的策略,策略名不分區大小寫
    private Dictionary<string, AuthorizationPolicy> PolicyMap { get; } = new Dictionary<string, AuthorizationPolicy>(StringComparer.OrdinalIgnoreCase);

    // 授權失敗後,後續的 IAuthorizationHandler 是否還繼續執行
    public bool InvokeHandlersAfterFailure { get; set; } = true;

    // 默認策略:身份認證通過的用戶
    public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();

    // 回退策略
    public AuthorizationPolicy? FallbackPolicy { get; set; }

    public void AddPolicy(string name, AuthorizationPolicy policy)
    {
        PolicyMap[name] = policy;
    }

    public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy)
    {
        var policyBuilder = new AuthorizationPolicyBuilder();
        configurePolicy(policyBuilder);
        PolicyMap[name] = policyBuilder.Build();
    }

    public AuthorizationPolicy? GetPolicy(string name)
    {
        if (PolicyMap.TryGetValue(name, out var value))
        {
            return value;
        }

        return null;
    }
}

默認策略與回退策略不同:

  • 默認策略,是指當接口標註了Authorize,但是未明確指定策略時,應使用的策略
  • 回退策略,是指當某個接口未標註Authorize時,應使用的策略,且該值是可以爲空的

接下來看中間件的註冊app.UseAuthorization()

public static class AuthorizationAppBuilderExtensions
{
    public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app)
    {
        VerifyServicesRegistered(app);

        return app.UseMiddleware<AuthorizationMiddleware>();
    }

    private static void VerifyServicesRegistered(IApplicationBuilder app)
    {
        if (app.ApplicationServices.GetService(typeof(AuthorizationPolicyMarkerService)) == null)
        {
            throw new InvalidOperationException(...);
        }
    }
}

internal class AuthorizationPolicyMarkerService
{
}

從這裏,我們得知了AuthorizationPolicyMarkerService的作用,就是爲了確保在註冊授權中間件之前,我們已經調用過了UseAuthorization,註冊了全部所需要的服務。

接下來,深入AuthorizationMiddleware的實現:

public class AuthorizationMiddleware
{
    private const string SuppressUseHttpContextAsAuthorizationResource = "Microsoft.AspNetCore.Authorization.SuppressUseHttpContextAsAuthorizationResource";

    private readonly RequestDelegate _next;
    private readonly IAuthorizationPolicyProvider _policyProvider;

    public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider) 
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
        _policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
    }

    public async Task Invoke(HttpContext context)
    {
        var endpoint = context.GetEndpoint();

        // ... 省略部分代碼

        // AuthorizeAttribute 就實現了接口 IAuthorizeData,從這裏也就可以得到我們的授權數據
        var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
        // 1. 將所有授權要求組裝到一個策略實例中
        var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData);
        // 無授權策略,則無需進行授權校驗
        if (policy == null)
        {
            await _next(context);
            return;
        }

        // IPolicyEvaluator 的默認聲明週期是 Transient,而該中間件的生命週期是 Singleton,
        // 所以該服務不建議注入到構造函數
        var policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>();

        // 2. 認證
        var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context);

        // 3. 如果標記了 AllowAnonymousAttribute 特性,則跳過授權校驗
        if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
        {
            await _next(context);
            return;
        }

        object? resource;
        if (AppContext.TryGetSwitch(SuppressUseHttpContextAsAuthorizationResource, out var useEndpointAsResource) && useEndpointAsResource)
        {
            resource = endpoint;
        }
        else
        {
            resource = context;
        }
        
        // 4. 授權
        var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource);
        // 5. 針對授權結果,進行不同的響應處理
        var authorizationMiddlewareResultHandler = context.RequestServices.GetRequiredService<IAuthorizationMiddlewareResultHandler>();
        await authorizationMiddlewareResultHandler.HandleAsync(_next, context, policy, authorizeResult);
    }
}

從這裏可以看出,授權的所有方式,都是基於策略來實現的。

下面我們一步步來分析它。先看第1步,瞭解它是如何將多種授權要求組裝爲一個策略的:

public class AuthorizationPolicy
{
    public static async Task<AuthorizationPolicy?> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
    {
        // ... 省略部分代碼
        
        AuthorizationPolicyBuilder? policyBuilder = null;

        foreach (var authorizeDatum in authorizeData)
        {
            if (policyBuilder == null)
            {
                policyBuilder = new AuthorizationPolicyBuilder();
            }

            // 先處理策略
            var useDefaultPolicy = true;
            if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
            {
                // 通過指定的策略名獲取策略實例
                var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy);
                if (policy == null)
                {
                    throw new InvalidOperationException(...);
                }
                policyBuilder.Combine(policy);
                useDefaultPolicy = false;
            }

            // 再處理角色
            var rolesSplit = authorizeDatum.Roles?.Split(',');
            if (rolesSplit?.Length > 0)
            {
                var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
                // 將角色要求添加到策略
                policyBuilder.RequireRole(trimmedRolesSplit);
                useDefaultPolicy = false;
            }

            // 最後處理認證方案
            var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(',');
            if (authTypesSplit?.Length > 0)
            {
                foreach (var authType in authTypesSplit)
                {
                    if (!string.IsNullOrWhiteSpace(authType))
                    {
                        // 將認證方案要求添加到策略
                        policyBuilder.AuthenticationSchemes.Add(authType.Trim());
                    }
                }
            }

            if (useDefaultPolicy)
            {
                // 添加默認策略
                policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync());
            }
        }

        // 如果此時還沒有策略,則查看是否存在回退策略,如果有,則返回
        if (policyBuilder == null)
        {
            var fallbackPolicy = await policyProvider.GetFallbackPolicyAsync();
            if (fallbackPolicy != null)
            {
                return fallbackPolicy;
            }
        }

        // 返回當前組裝的策略實例
        return policyBuilder?.Build();
    }
}

整體邏輯已經通過註釋給出了,就不多做解釋了。我們來看一下IAuthorizationPolicyProvider,在之前我們就已經認識它了,這裏也用到了:

public interface IAuthorizationPolicyProvider
{
    Task<AuthorizationPolicy?> GetPolicyAsync(string policyName);

    Task<AuthorizationPolicy> GetDefaultPolicyAsync();

    Task<AuthorizationPolicy?> GetFallbackPolicyAsync();
}

從名字我們可以看出,該接口用於提供授權策略實例。

該接口有三個方法:

  • GetPolicyAsync:根據策略名獲取策略實例
  • GetDefaultPolicyAsync:獲取默認策略,當我們指明瞭要進行授權校驗,但沒有設定任何授權要求(如策略名、角色、身份認證方案等)時,會使用默認策略。
  • GetFallbackPolicyAsync:獲取回退策略,當我們沒有指定任何授權校驗時,會使用回退策略。如果回退策略爲null,則跳過授權校驗。

下面就看下該接口的默認實現DefaultAuthorizationPolicyProvider

public class DefaultAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
    private readonly AuthorizationOptions _options;
    private Task<AuthorizationPolicy>? _cachedDefaultPolicy;
    private Task<AuthorizationPolicy?>? _cachedFallbackPolicy;

    public DefaultAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
    {
        _options = options.Value;
    }
    
    public virtual Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        // 從 AuthorizationOptions 中查找已添加的策略實例
        return Task.FromResult(_options.GetPolicy(policyName));
    }

    public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
    {
        // 取 AuthorizationOptions 中配置的 DefaultPolicy
        if (_cachedDefaultPolicy == null || _cachedDefaultPolicy.Result != _options.DefaultPolicy)
        {
            _cachedDefaultPolicy = Task.FromResult(_options.DefaultPolicy);
        }

        return _cachedDefaultPolicy;
    }

    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
    {
        // 取 AuthorizationOptions 中配置的 FallbackPolicy
        if (_cachedFallbackPolicy == null || _cachedFallbackPolicy.Result != _options.FallbackPolicy)
        {
            _cachedFallbackPolicy = Task.FromResult(_options.FallbackPolicy);
        }

        return _cachedFallbackPolicy;
    }
}

OK,IAuthorizationPolicyProvider我們就看到這。

下面,我們回到AuthorizationMiddleware,繼續往下來到第2步,出現了新接口IPolicyEvaluator

public interface IPolicyEvaluator
{
    Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context);

    Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource);
}

該接口用於評估身份認證和授權結果,分別產出AuthenticateResultPolicyAuthorizationResult

該接口有兩個方法:

  • AuthenticateAsync:根據策略中提供的方案進行身份認證,生成認證結果
  • AuthorizeAsync:根據策略和認證結果進行授權,生成授權結果

該接口的默認實現類爲PolicyEvaluator

public class PolicyEvaluator : IPolicyEvaluator
{
    private readonly IAuthorizationService _authorization;

    public PolicyEvaluator(IAuthorizationService authorization)
    {
        _authorization = authorization;
    }

    public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
    {
        // 策略中指定了身份認證方案
        if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0)
        {
            // 將多個身份認證方案的結果進行合併
            ClaimsPrincipal? newPrincipal = null;
            foreach (var scheme in policy.AuthenticationSchemes)
            {
                var result = await context.AuthenticateAsync(scheme);
                if (result != null && result.Succeeded)
                {
                    newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);
                }
            }

            if (newPrincipal != null)
            {
                context.User = newPrincipal;
                return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes)));
            }
            else
            {
                context.User = new ClaimsPrincipal(new ClaimsIdentity());
                return AuthenticateResult.NoResult();
            }
        }

        // 是否通過了默認的身份認證方案
        return (context.User?.Identity?.IsAuthenticated ?? false)
            ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User"))
            : AuthenticateResult.NoResult();
    }

    public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource)
    {
        var result = await _authorization.AuthorizeAsync(context.User, resource, policy);
        if (result.Succeeded)
        {
            return PolicyAuthorizationResult.Success();
        }

        // 授權失敗時:
        //      若身份認證通過,則返回Forbid
        //      若身份認證未通過,則發出質詢
        return (authenticationResult.Succeeded)
            ? PolicyAuthorizationResult.Forbid(result.Failure)
            : PolicyAuthorizationResult.Challenge();
    }
}

從這裏,我們可以看出,如果默認的身份認證方案無法提供完整的身份認證,可以在IAuthorizeData中指定AuthenticationSchemes,通過它來重新進行身份認證。

這裏面使用到了新的接口IAuthorizationService,從名字也可以看出它是專門用來做授權的服務接口,真正的授權邏輯代碼被封裝到了該接口的實現類中,我們看下它的定義:

public interface IAuthorizationService
{
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements);

    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName);
}

該接口具有一個方法AuthorizeAsync的兩種重載:

  • 檢查用戶是否滿足指定資源(resource)的特定要求(requirements)
  • 檢查用戶是否滿足特定的授權策略

如果你足夠細心,你會發現這兩個重載並不能滿足上方代碼的調用,因爲調用時第三個參數我們傳遞的是AuthorizationPolicy類型,其實啊,它是被放到了擴展方法中。

public static class AuthorizationServiceExtensions
{
    public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object? resource, AuthorizationPolicy policy)
    {
        return service.AuthorizeAsync(user, resource, policy.Requirements);
    }
}

所以,從這裏我們就知道了,它調用的實際上是第一個重載。

該接口的默認實現爲DefaultAuthorizationService

public class DefaultAuthorizationService : IAuthorizationService
{
    // 以下字段均爲構造函數注入
    private readonly AuthorizationOptions _options;
    private readonly IAuthorizationHandlerContextFactory _contextFactory;
    private readonly IAuthorizationHandlerProvider _handlers;
    private readonly IAuthorizationEvaluator _evaluator;
    private readonly IAuthorizationPolicyProvider _policyProvider;

    public virtual async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements)
    {
        var authContext = _contextFactory.CreateContext(requirements, user, resource);
        var handlers = await _handlers.GetHandlersAsync(authContext);
        foreach (var handler in handlers)
        {
            await handler.HandleAsync(authContext);
            // 若配置爲授權失敗後不在調用後續Handlers
            if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed)
            {
                break;
            }
        }

        var result = _evaluator.Evaluate(authContext);

        // 省略一些代碼...

        return result;
    }

    public virtual async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName)
    {
        var policy = await _policyProvider.GetPolicyAsync(policyName);
        if (policy == null)
        {
            throw new InvalidOperationException($"No policy found: {policyName}.");
        }
        return await this.AuthorizeAsync(user, resource, policy);
    }
}

首先,這裏用到了IAuthorizationHandlerContextFactory,它用來創建授權處理器上下文:

public interface IAuthorizationHandlerContextFactory
{
    AuthorizationHandlerContext CreateContext(IEnumerable<IAuthorizationRequirement> requirements, ClaimsPrincipal user, object? resource);
}

public class DefaultAuthorizationHandlerContextFactory : IAuthorizationHandlerContextFactory
{
    public virtual AuthorizationHandlerContext CreateContext(IEnumerable<IAuthorizationRequirement> requirements, ClaimsPrincipal user, object? resource)
    {
        return new AuthorizationHandlerContext(requirements, user, resource);
    }
}

然後,下面用到了IAuthorizationHandlerProvider,它用來提供Handler,這些Handler包括我們之前實現的MinimumAgeAuthorizationHandler等。

public interface IAuthorizationHandlerProvider
{
    Task<IEnumerable<IAuthorizationHandler>> GetHandlersAsync(AuthorizationHandlerContext context);
}

public class DefaultAuthorizationHandlerProvider : IAuthorizationHandlerProvider
{
    private readonly IEnumerable<IAuthorizationHandler> _handlers;

    public DefaultAuthorizationHandlerProvider(IEnumerable<IAuthorizationHandler> handlers)
    {
        _handlers = handlers;
    }

    public Task<IEnumerable<IAuthorizationHandler>> GetHandlersAsync(AuthorizationHandlerContext context)
        => Task.FromResult(_handlers);
}

另外,這裏還用到了IAuthorizationEvaluator,該接口用於評估授權結果是成功還是失敗,並將結果構造爲AuthorizationResult實例。

public interface IAuthorizationEvaluator
{
    AuthorizationResult Evaluate(AuthorizationHandlerContext context);
}

public class DefaultAuthorizationEvaluator : IAuthorizationEvaluator
{
    public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
        => context.HasSucceeded
            ? AuthorizationResult.Success()
            : AuthorizationResult.Failed(context.HasFailed
                ? AuthorizationFailure.ExplicitFail()
                : AuthorizationFailure.Failed(context.PendingRequirements));
}

最後,獲取到授權結果AuthorizationResult後,我們就來到了第5步,由IAuthorizationMiddlewareResultHandler針對不同的授權結果進行響應處理。

public interface IAuthorizationMiddlewareResultHandler
{
    Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult);
}

public class AuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
    public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    {
        // 需要發出質詢
        if (authorizeResult.Challenged)
        {
            if (policy.AuthenticationSchemes.Count > 0)
            {
                foreach (var scheme in policy.AuthenticationSchemes)
                {
                    await context.ChallengeAsync(scheme);
                }
            }
            else
            {
                await context.ChallengeAsync();
            }

            return;
        }
        // 需要響應403
        else if (authorizeResult.Forbidden)
        {
            if (policy.AuthenticationSchemes.Count > 0)
            {
                foreach (var scheme in policy.AuthenticationSchemes)
                {
                    await context.ForbidAsync(scheme);
                }
            }
            else
            {
                await context.ForbidAsync();
            }

            return;
        }

        // 授權通過,繼續執行管道
        await next(context);
    }
}

至此,容器中註冊的幾個服務均涉及到了,我們再來總結一下:

  • AuthorizationPolicyMarkerService:用於標誌已經調用過了UseAuthorization,註冊了授權所需要的全部服務。
  • IAuthorizationService:默認實現爲DefaultAuthorizationService,用於對用戶進行授權(Authorize)。
  • IAuthorizationHandlerContextFactory:默認實現爲DefaultAuthorizationHandlerContextFactory,用於創建授權處理器上下文。
  • IAuthorizationHandlerProvider:默認實現爲DefaultAuthorizationHandlerProvider,用於提供用戶授權的處理器(IAuthorizationHandler)
  • IAuthorizationHandler:默認實現爲PassThroughAuthorizationHandler(處理自身既是Requirement,又是Handler的類),用於提供Requirement的處理邏輯。
  • IAuthorizationPolicyProvider:默認實現爲DefaultAuthorizationPolicyProvider,用於提供授權策略實例(AuthorizationPolicy)。
  • IAuthorizationEvaluator:默認實現爲DefaultAuthorizationEvaluator,用於評估授權結果是成功還是失敗,並將結果構造爲AuthorizationResult實例。
  • IPolicyEvaluator:默認實現爲PolicyEvaluator,用於評估身份認證和授權結果
  • IAuthorizationMiddlewareResultHandler:默認實現爲AuthorizationMiddlewareResultHandler,用於針對授權結果,進行不同的響應處理。

這下,當你要實現自定義操作時,只需要重寫對應接口的實現就好啦。

爲了方便大家理解,我將各個接口的調用關係畫了一張圖:

最後,大家肯定知道還有一個可以控制權限的地方,就是IAuthorizationFilter過濾器。不過,如果沒有必要,我並不推薦你使用它。因爲它是mvc時代的舊產物,而且你要自己來實現一套完整的授權框架。

補充

根據我的經驗,大家用的比較多的授權方案是基於權限Key的,爲此,我也寫了一個簡單的示例程序,供大家參考:XXTk.Auth.Samples.Permission.HttpApi

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章