ASP.NET Core 认证与授权[5]:初识授权

经过前面几章的姗姗学步,我们了解了在 ASP.NET Core 中是如何认证的,终于来到了授权阶段。在认证阶段我们通过用户令牌获取到用户的Claims,而授权便是对这些的Claims的验证,如:是否拥有Admin的角色,姓名是否叫XXX等等。本章就来介绍一下 ASP.NET Core 的授权系统的简单使用。

目录

  1. 简单授权
  2. 授权策略详解
  3. 基于策略的授权进阶

简单授权

在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() { } }

如上,LoginAction便不再需要授权,同样,在 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中定义的PolicyRolesAuthenticationSchemes三个属性分别代表着 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中主要涉及到AuthorizationPolicyAuthorizationPolicyBuilder两个对象。

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并不属于基于资源的授权,所以不在这里,其用法留在其后续章节再来介绍。

整个授权策略的内容也就这么多,并不复杂,整个结构大致如下:

authorization_policy

基于策略的授权进阶

在上一小节,我们探索了一下授权策略的源码,现在就来实战一下。

我们使用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 授权系统的执行流程。

 
分类:  .NET CoreASP.NET Core

 

 

转载于:https://www.cnblogs.com/webenh/p/11579736.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值