經過前面幾章的姍姍學步,我們瞭解了在 ASP.NET Core 中是如何認證的,終於來到了授權階段。在認證階段我們通過用戶令牌獲取到用戶的Claims,而授權便是對這些的Claims的驗證,如:是否擁有Admin的角色,姓名是否叫XXX等等。本章就來介紹一下 ASP.NET Core 的授權系統的簡 ...
經過前面幾章的姍姍學步,我們瞭解了在 ASP.NET Core 中是如何認證的,終於來到了授權階段。在認證階段我們通過用戶令牌獲取到用戶的Claims,而授權便是對這些的Claims的驗證,如:是否擁有Admin的角色,姓名是否叫XXX等等。本章就來介紹一下 ASP.NET Core 的授權系統的簡單使用。
目錄
簡單授權
在ASP.NET 4.x中,我們通常使用Authorize
過濾器來進行授權,它可以作用在Controller和Action上面,也可以添加到全局過濾器中。而在ASP.NET Core中也有一個Authorize
特性(但不是過濾器),用法類似:
[Authorize] // Controller級別
public class SampleDataController : Controller
{
[Authorize] // Action級別
public IActionResult SampleAction()
{
}
}
IAllowAnonymous
在ASP.NET 4.x中,我們最常用的另一個特性便是AllowAnonymous
,用來設置某個Controller或者Action跳過授權,它在 ASP.NET Core 中同樣適用:
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}
public ActionResult Logout()
{
}
}
如上,Login
Action便不再需要授權,同樣,在 ASP.NET Core 中提供了一個統一的IAllowAnonymous
介面,在授權邏輯中都是通過該介面來判斷是否跳過授權驗證的。
public interface IAllowAnonymous
{
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AllowAnonymousAttribute : Attribute, IAllowAnonymous
{
}
IAuthorizeData
上面提到,在 ASP.NET Core 中,AuthorizeAttribute
不再是一個MVC中的Filter
了,而只是一個簡單的實現了IAuthorizeData
介面的Attribute:
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; }
}
記得第一次在ASP.NET Core中實現自定義授權時,按照以前的經驗,直接繼承自AuthorizeAttribute
,然後準備重寫OnAuthorization
方法,結果懵逼了。然後在MVC的源碼中,苦苦搜尋AuthorizeAttribute
的蹤跡,卻毫無所獲,後來才註意到它實現了IAuthorizeData
介面,該介面才是認證的源頭,而Authorize特性只是認證信息的載體,並不包含任何邏輯。IAuthorizeData
中定義的Policy
, Roles
, AuthenticationSchemes
三個屬性分別代表著 ASP.NET Core 授權系統中的三種授權方式。
基於角色的授權
基於角色的授權,我們都比較熟悉,使用方式如下:
[Authorize(Roles = "Admin")] // 多個Role可以使用,分割
public class SampleDataController : Controller
{
...
}
基於角色的授權的邏輯與ASP.NET 4.x類似,都是使用我在《初識認證》中介紹的IsInRole
方法來實現的。
基於Scheme的授權
對於AuthenticationScheme我在前面幾章也都介紹過,比如Cookie認證預設使用的AuthenticationScheme就是Cookies
,在JwtBearer認證中,預設的Scheme就是Bearer
。
當初在學習認證時,還在疑惑,如何在使用Cookie認證的同時又支持Bearer認證呢?因為在認證中只能設置一個Scheme來執行,當看到這裡豁然開朗,後面會詳細介紹。
[Authorize(AuthenticationSchemes = "Cookies")] // 多個Scheme可以使用,分割
public class SampleDataController : Controller
{
...
}
當我們的應用程式中,同時使用了多種認證Scheme時,AuthenticationScheme授權就非常有用,在該授權模式下,會通過context.AuthenticateAsync(scheme)
重新獲取Claims。
基於策略的授權
在ASP.NET Core中,重新設計了一種更加靈活的授權方式:基於策略的授權,也是授權的核心。
在使用基於策略的授權時,首先要定義授權策略,而授權策略本質上就是對Claims的一系列斷言。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
}
如上,我們定義了一個名稱為EmployeeOnly
的授權策略,它要求用戶的Claims中必須包含類型為EmployeeNumber
的Claim。
其實,基於角色的授權和基於Scheme的授權,只是一種語法上的便捷,最終都會生成授權策略,後文會詳解介紹。
然後便可以在Authorize
特性中通過Policy
屬性來指定授權策略:
[Authorize(Policy = "EmployeeOnly")]
public class SampleDataController : Controller
{
}
授權策略詳解
AddAuthorization
授權策略的定義使用了AddAuthorization
擴展方法,我們來看看它的源碼:
public static class AuthorizationServiceCollectionExtensions
{
public static IServiceCollection AddAuthorization(this IServiceCollection services)
{
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 AddAuthorization(this IServiceCollection services, Action<AuthorizationOptions> configure)
{
services.Configure(configure);
return services.AddAuthorization();
}
}
首先,是對授權進行配置的AuthorizationOptions
,然後在DI系統中註冊了幾個核心對象的預設實現,我們一一來看。
AuthorizationOptions
對於Options模式,大家應該都比較熟悉了,AuthorizationOptions
是添加和獲取授權策略的入口點:
public class AuthorizationOptions
{
private IDictionary<string, AuthorizationPolicy> PolicyMap { get; } = new Dictionary<string, AuthorizationPolicy>(StringComparer.OrdinalIgnoreCase);
// 在上一個策略驗證失敗後,是否繼續執行下一個授權策略
public bool InvokeHandlersAfterFailure { get; set; } = true;
public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
public void AddPolicy(string name, AuthorizationPolicy policy)
{
PolicyMap[name] = policy;
}
public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy)
{
var policyBuilder = new AuthorizationPolicyBuilder();
configurePolicy(policyBuilder);
AddPolicy(name,policyBuilder.Build());
}
public AuthorizationPolicy GetPolicy(string name)
{
return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null;
}
}
首先是一個PolicyMap
字典,我們定義的策略都保存在其中,AddPolicy
方法只是簡單的將策略添加到該字典中,而其DefaultPolicy
屬性表示預設策略,初始值為:“已認證用戶”。
在AuthorizationOptions
中主要涉及到AuthorizationPolicy
和AuthorizationPolicyBuilder
兩個對象。
AuthorizationPolicy
在 ASP.NET Core 中,授權策略具體表現為一個AuthorizationPolicy
對象:
public class AuthorizationPolicy
{
public AuthorizationPolicy(IEnumerable<IAuthorizationRequirement> requirements, IEnumerable<string> authenticationSchemes) {}
public IReadOnlyList<IAuthorizationRequirement> Requirements { get; }
public IReadOnlyList<string> AuthenticationSchemes { get; }
public static AuthorizationPolicy Combine(params AuthorizationPolicy[] policies)
{
return Combine((IEnumerable<AuthorizationPolicy>)policies);
}
public static AuthorizationPolicy Combine(IEnumerable<AuthorizationPolicy> policies)
{
foreach (var policy in policies)
{
builder.Combine(policy);
}
return builder.Build();
}
public static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
{
foreach (var authorizeDatum in authorizeData)
{
any = true;
var useDefaultPolicy = true;
if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
{
policyBuilder.Combine(await policyProvider.GetPolicyAsync(authorizeDatum.Policy));
useDefaultPolicy = false;
}
var rolesSplit = authorizeDatum.Roles?.Split(',');
if (rolesSplit != null && rolesSplit.Any())
{
policyBuilder.RequireRole(rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()));
useDefaultPolicy = false;
}
var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(',');
if (authTypesSplit != null && authTypesSplit.Any())
{
foreach (var authType in authTypesSplit)
{
if (!string.IsNullOrWhiteSpace(authType))
{
policyBuilder.AuthenticationSchemes.Add(authType.Trim());
}
}
}
if (useDefaultPolicy)
{
policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync());
}
}
return any ? policyBuilder.Build() : null;
}
}
如上,Combine
方法通過調用AuthorizationPolicyBuilder
來完成授權策略的合併,而CombineAsync
則是將我們上面介紹的IAuthorizeData
轉換為授權策略,因此上面說基於角色/Scheme的授權本質上都是基於策略的授權。
對於AuthenticationSchemes
屬性,我們在前幾章介紹認證時經常看到,用來表示我們使用哪個認證Scheme來獲取用戶的Claims,如果指定多個,則會合併它們的Claims,其實現下一章再來介紹。
而Requirements
屬性則是策略的核心了,每一個Requirement都代表一個授權條件,我們就先來瞭解一下它。
IAuthorizationRequirement
Requirement使用IAuthorizationRequirement
介面來表示:
public interface IAuthorizationRequirement
{
}
IAuthorizationRequirement介面中並沒有任何成員,在 ASP.NET Core 中內置了一些常用的實現:
AssertionRequirement :使用最原始的斷言形式來聲明授權策略。
DenyAnonymousAuthorizationRequirement :用於表示禁止匿名用戶訪問的授權策略,併在
AuthorizationOptions
中將其設置為預設策略。ClaimsAuthorizationRequirement :用於表示判斷Cliams中是否包含預期的Claims的授權策略。
RolesAuthorizationRequirement :用於表示使用
ClaimsPrincipal.IsInRole
來判斷是否包含預期的Role的授權策略。NameAuthorizationRequirement:用於表示使用
ClaimsPrincipal.Identities.Name
來判斷是否包含預期的Name的授權策略。OperationAuthorizationRequirement:用於表示基於操作的授權策略。
其邏輯也都非常簡單,我就不再一一介紹,只展示一下RolesAuthorizationRequirement
的代碼片段:
public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement
{
public IEnumerable<string> AllowedRoles { get; }
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement)
{
...
if (requirement.AllowedRoles.Any(r => context.User.IsInRole(r)))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
其AllowedRoles
表示允許授權通過的角色,而它還實現了IAuthorizationHandler
介面,用來完成授權的邏輯。
public interface IAuthorizationHandler
{
Task HandleAsync(AuthorizationHandlerContext context);
}
AuthorizationRequirement並不是一定要實現
IAuthorizationHandler
介面,後文會詳細介紹。
AuthorizationPolicyBuilder
在上面已經多次用到AuthorizationPolicyBuilder
,它提供了一系列創建AuthorizationPolicy
的快捷方法:
public class AuthorizationPolicyBuilder
{
public AuthorizationPolicyBuilder(params string[] authenticationSchemes);
public AuthorizationPolicyBuilder(AuthorizationPolicy policy);
public IList<IAuthorizationRequirement> Requirements { get; set; }
public IList<string> AuthenticationSchemes { get; set; }
public AuthorizationPolicyBuilder AddAuthenticationSchemes(params string[] schemes);
public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequirement[] requirements);
public AuthorizationPolicyBuilder RequireAssertion(Func<AuthorizationHandlerContext, bool> handler);
public AuthorizationPolicyBuilder RequireAssertion(Func<AuthorizationHandlerContext, Task<bool>> handler)
{
Requirements.Add(new AssertionRequirement(handler));
return this;
}
public AuthorizationPolicyBuilder RequireAuthenticatedUser()
{
Requirements.Add(new DenyAnonymousAuthorizationRequirement());
return this;
}
public AuthorizationPolicyBuilder RequireClaim(string claimType);
public AuthorizationPolicyBuilder RequireClaim(string claimType, params string[] requiredValues);
public AuthorizationPolicyBuilder RequireClaim(string claimType, IEnumerable<string> requiredValues)
{
Requirements.Add(new ClaimsAuthorizationRequirement(claimType, requiredValues));
return this;
}
public AuthorizationPolicyBuilder RequireRole(params string[] roles);
public AuthorizationPolicyBuilder RequireRole(IEnumerable<string> roles)
{
Requirements.Add(new RolesAuthorizationRequirement(roles));
return this;
}
public AuthorizationPolicyBuilder RequireUserName(string userName)
{
Requirements.Add(new NameAuthorizationRequirement(userName));
return this;
}
public AuthorizationPolicy Build();
public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy);
}
在上面介紹的幾個Requirement,除了OperationAuthorizationRequirement
外,都有對應的快捷添加方法,由於OperationAuthorizationRequirement
並不屬於基於資源的授權,所以不在這裡,其用法留在其後續章節再來介紹。
整個授權策略的內容也就這麼多,並不複雜,整個結構大致如下:
基於策略的授權進階
在上一小節,我們探索了一下授權策略的源碼,現在就來實戰一下。
我們使用AuthorizationPolicyBuilder
可以很容易的在策略定義中組合我們需要的Requirement:
public void ConfigureServices(IServiceCollection services)
{
var commonPolicy = new AuthorizationPolicyBuilder().RequireClaim("MyType").Build();
services.AddAuthorization(options =>
{
options.AddPolicy("User", policy => policy
.RequireAssertion(context => context.User.HasClaim(c => (c.Type == "EmployeeNumber" || c.Type == "Role")))
);
options.AddPolicy("Employee", policy => policy
.RequireRole("Admin")
.RequireUserName("Alice")
.RequireClaim("EmployeeNumber")
.Combine(commonPolicy));
});
}
如上,如果需要,我們還可以定義一個公共的策略對象,然後在策略定義中直接將其合併進來。
自定義策略
當內置的Requirement不能滿足我們的需求時,我們也可以很容易的定義自己的Requirement:
public class MinimumAgeRequirement : AuthorizationHandler<NameAuthorizationRequirement>, IAuthorizationRequirement
{
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
public int MinimumAge { get; private set; }
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NameAuthorizationRequirement requirement)
{
if (context.User != null && context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth)
{
var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);
int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
{
calculatedAge--;
}
if (calculatedAge >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
然後就可以直接在AddPolicy
中使用了:
services.AddAuthorization(options =>
{
options.AddPolicy("Over21", policy => policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
我們自定義的 Requirement 若想得到 ASP.NET Core 授權系統的執行,除了上面示例中的實現IAuthorizationHandler
介面外,也可以單獨定義AuthorizationHandler,這樣可以更好的使用DI系統,並且還可以定義多個Handler,下麵就來演示一下。
多Handler模式
授權策略中的多個Requirement,它們屬於 & 的關係,只用全部驗證通過,才能最終授權成功。但是在有些場景下,我們可能希望一個授權策略可以適用多種情況,比如,我們進入公司時需要出示員工卡才可以被授權進入,但是如果我們忘了帶員工卡,可以去申請一個臨時卡,同樣可以授權成功:
public class EnterBuildingRequirement : IAuthorizationRequirement
{
}
public class BadgeEntryHandler : AuthorizationHandler<EnterBuildingRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EnterBuildingRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == ClaimTypes.BadgeId)
{
context.Succeed(requirement);
}
else
{
// context.Fail();
}
return Task.CompletedTask;
}
}
public class HasTemporaryStickerHandler : AuthorizationHandler<EnterBuildingRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EnterBuildingRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == ClaimTypes.TemporaryBadgeId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
如上,我們定義了兩個Handler,但是想讓它們得到執行,還需要將其註冊到DI系統中:
services.AddSingleton<IAuthorizationHandler, BadgeEntryHandler>();
services.AddSingleton<IAuthorizationHandler, HasTemporaryStickerHandler>();
此時,在我們的應該程式中使用EnterBuildingRequirement
的授權時,將會依次執行這兩個Handler。而在上面介紹AuthorizationOptions
時,提到它還有一個InvokeHandlersAfterFailure
屬性,在這裡就派上用場了,只有其為true
時(預設為True),才會在當前 AuthorizationHandler 授權失敗時,繼續執行下一個 AuthorizationHandler。
在上面的示例中,我們使用context.Succeed(requirement)
將授權結果設置為成功,而失敗時並沒有做任何標記,正常情況下都是這樣做的。但是如果需要,我們可以通過調用context.Fail()
方法顯式的將授權結果設置為失敗,那麼,不管其他 AuthorizationHandler 是成功還是失敗,最終結果都將是授權失敗。
總結
ASP.NET Core 授權策略是一種非常強大、靈活的許可權驗證方案,能夠滿足大部分的授權場景。通過本文對授權策略的詳細介紹,我想應該能夠靈活的使用基於策略的授權了,但是授權策略到底是怎麼執行的呢?在下一章中,就來完整的探索一下 ASP.NET Core 授權系統的執行流程。