注:本文隸屬於《理解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
新增到了請求管道。
基於角色的授權
顧名思義,基於角色的授權就是檢查使用者是否擁有指定角色,如果是則授權通過,否則不通過。
我們先看一個簡單的例子:
[Authorize(Roles = "Admin")]
public string GetForAdmin()
{
return "Admin only";
}
這裡,我們將AuthorizeAttribute
特性的Roles
屬性設定為了Admin
,也就是說,如果使用者想要訪問GetForAdmin
介面,則必須擁有角色Admin。
如果某個介面想要允許多個角色訪問,該怎麼做呢?很簡單,通過英文逗號(,)分隔多個角色即可:
[Authorize(Roles = "Developer,Tester")]
public string GetForDeveloperOrTester()
{
return "Developer || Tester";
}
就像上面這樣,通過逗號將Developer
和Tester
分隔開來,當接到請求時,若使用者擁有角色Developer和Tester其一,就允許訪問該介面。
最後,如果某個介面要求使用者必須同時擁有多個角色時才允許訪問,那我們可以通過新增多個AuthorizeAttribute
特性來達到目的:
[Authorize(Roles = "Developer")]
[Authorize(Roles = "Tester")]
public string GetForDeveloperAndTester()
{
return "Developer && Tester";
}
只有當使用者同時擁有角色Developer
和Tester
時,才允許訪問該介面。
你現在可能已經迫不及待要親自驗證一下了,不過你還記得如何設定使用者的角色嗎?我們在身份認證的文章中介紹過,在頒發身份票據時,可以通過宣告新增角色,例如:
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();
}
由於篇幅限制,其他的登入程式碼就不貼了,可以在示例程式中找到。
基於宣告的授權
上面介紹的基於角色的授權,實際上就是基於宣告中的“角色”來實現的,而基於宣告的授權,則將範圍擴充套件到了所有宣告(而不僅僅是角色)。
基於宣告的授權,是在基於策略的授權基礎上實現的。為什麼這麼說呢?因為我們需要通過新增策略來使用宣告:
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"));
我們新增了兩條策略:RankClaimP3
和RankClaimM3
,除了要求使用者具有宣告Rank
外,還分別要求Rank的值為P3
和M3
。
類似於基於角色的宣告,我們也可以新增“Or”、“And”邏輯的策略:
options.AddPolicy("RankClaimP3OrM3", policy => policy.RequireClaim("Rank", "P3", "M3"));
options.AddPolicy("RankClaimP3AndM3", policy => policy.RequireClaim("Rank", "P3").RequireClaim("Rank", "M3"));
策略RankClaimP3OrM3
要求使用者具有宣告Rank
,且值為P3
或M3
即可;而策略RankClaimP3AndM3
要求使用者具有宣告Rank
,且值必須同時包含P3
和M3
。
策略的用法與之前的類似(注意策略不能像角色一樣通過逗號分隔):
// 僅要求使用者具有宣告“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")));
基於策略的授權
通常來說,以上兩種授權方式僅適用於較為簡單的業務場景,而當業務場景比較複雜時,它倆就顯得無能為力了。因此,我們必須能夠設計更加自由的策略,也就是基於策略的授權。
基於策略的授權,我打算將其分成兩種型別來介紹:簡單策略和動態策略。
簡單策略
在上面,我們制定策略時,使用了大量的RequireXXX
,我們也希望能夠將自定義策略封裝一下,當然,你可以寫一些擴充套件方法,不過我更加推薦使用IAuthorizationRequirement
和IAuthorizationHandler
。
現在,我們虛構一個場景:網咖管理,未滿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歲,並將其新增到了policy
的Requirements
屬性中。
你可以寫一個類似的介面進行測試:
[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
這裡面有幾個介面是我們之前見過的,比如IAuthorizationPolicyProvider
、IAuthorizationHandler
。不著急研究其他幾個介面的作用,我們們接著看下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);
}
該介面用於評估身份認證和授權結果,分別產出AuthenticateResult
和PolicyAuthorizationResult
。
該介面有兩個方法:
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