今天我们一起来探索一下ASP.NET Core框架中的Authorization。我们知道请求进入管道处理流程先会使用Authentication进行用户认证,然后使用Authorization进行用户授权。如果没有看过认证过程的大家可以先转到Authentication这一篇。
AddAuthorization首先还是一样的方式,在管道中需要使用Authorization服务,我们首先需要向容器中添加相关服务,然后在管道处理中使用UseAuthorization,有人可能会比较疑惑,为什么框架自动生成好的项目中只有UseAuthorization而没有看到AddAuthorization这样的代码呢?
1 private static IMvcCoreBuilder AddControllersCore(IServiceCollection services) 2 { 3 return services.AddMvcCore().AddAuthorization(); 4 } 5 6 public static IServiceCollection AddAuthorizationCore(this IServiceCollection services) 7 { 8 if (services == null) 9 { 10 throw new ArgumentNullException(nameof(services)); 11 } 12 13 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationService>()); 14 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationPolicyProvider, DefaultAuthorizationPolicyProvider>()); 15 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerProvider, DefaultAuthorizationHandlerProvider>()); 16 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationEvaluator, DefaultAuthorizationEvaluator>()); 17 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerContextFactory, DefaultAuthorizationHandlerContextFactory>()); 18 services.TryAddEnumerable(ServiceDescriptor.Transient<IAuthorizationHandler, PassThroughAuthorizationHandler>()); 19 return services; 20 }
通过源码我们可以看到这个过程其实是在添加MVC服务的时候做的,而AddAuthorization就是把用户授权过程中必要的一些服务注入到容器。
下面我们来看在管道中添加的处理程序UseAuthorization,去了解一下框架是如何进行用户授权的。
UseAuthorization
在管道处理中,授权过程的逻辑是定义在AuthorizationMiddleware中间件里面的。当用户请求某个资源时处理过程来到管道时,中间件中必须要先看一下这个资源是否需要授权,如果使用了AuthorizeAttribute进行标记,那么表明就是需要授权的,所以最先的步骤是需要拿到用户添加在
AuthorizeAttribute中的信息,而MVC流程中控制器和action相关的信息会被保存在获取的Endpoint的元数据上,所以第一步就是从元数据中获取到IAuthorizeData的信息。
下面我们先学习一下授权中的几个重点对象的概念:
IAuthorizeData
我们知道,如果需要对某个请求的资源开启授权校验,就要在某个控制器或者action上添加Authorize的特性,比如需要角色名称是管理员,我们一般会加上[Authorize(Roles ="admin")]。
我们来看一下AuthorizeAttribute这个特性的源码:
1 public class AuthorizeAttribute : Attribute, IAuthorizeData 2 { 3 ... 4 5 public string Policy { get; set; } 6 7 public string Roles { get; set; } 8 9 public string AuthenticationSchemes { get; set; } 10 }
可以看到,特性继承于IAuthorizeData接口,该特性中定义有三个属性:
Policy:用于定义授权基于的策略名称
Roles:用于定义授权基于的角色名称
AuthenticationSchemes:用于定义采用该授权方式前使用的用户认证方案
这三个属性正是IAuthorizeData接口中定义的属性,在管道中经过UseRouting中间件的处理匹配到合适的终结点时,请求资源上添加的IAuthorizeData信息将会被添加到终结点的元数据中。
IAuthorizationRequirement和AuthorizationHandler<TRequirement>
IAuthorizationRequirement接口是一个空接口,无实际用处,只是用来进行标记其实现类是一个授权规则。
AuthorizationHandler<TRequirement>是一个用于定义授权处理规则的抽象基类,他继承于IAuthorizationHandler接口,该接口只有一个HandleAsync的接口,系统请求的资源需要什么授权规则即是通过继承此抽象基类重写HandleAsync进行定义的,开发人员可以根据具体的场景定义授权规则。
我们拿系统自带的角色授权规则RolesAuthorizationRequirement来看,我们希望基于roleName进行授权,于是继承AuthorizationHandler<RolesAuthorizationRequirement>和IAuthorizationRequirement,在授权处理中,通过判断User用户信息中是否包含指定的角色名称来返回授权是否成功,下面是RolesAuthorizationRequirement的源码:
1 public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement 2 { 3 public RolesAuthorizationRequirement(IEnumerable<string> allowedRoles) 4 { 5 if (allowedRoles == null) 6 { 7 throw new ArgumentNullException(nameof(allowedRoles)); 8 } 9 10 if (allowedRoles.Count() == 0) 11 { 12 throw new InvalidOperationException(Resources.Exception_RoleRequirementEmpty); 13 } 14 AllowedRoles = allowedRoles; 15 } 16 17 public IEnumerable<string> AllowedRoles { get; } 18 19 protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) 20 { 21 if (context.User != null) 22 { 23 bool found = false; 24 if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any()) 25 { 26 // Review: What do we want to do here? No roles requested is auto success? 27 } 28 else 29 { 30 found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r)); 31 } 32 if (found) 33 { 34 context.Succeed(requirement); 35 } 36 } 37 return Task.CompletedTask; 38 } 39 40 }
在实际业务场景中,涉及到的授权规则可能是多种多样的,有可能希望用户性别是女生,也有可能需要用户年龄满18岁,还有可能是多种条件限制的复杂情况等等,所以我们就可以仿照角色授权规则去自定义Requirement类继承此抽象类和接口。
AuthorizationPolicy
由于在授权的过程中,某个资源的授权规则可能不止一个,而是需要满足多个授权规则,于是我们需要有一个能够表明某次请求需要的授权规则的集合,这就是AuthorizationPolicy的作用了,我们先看源码:
1 public class AuthorizationPolicy 2 { 3 /// <summary> 4 /// Creates a new instance of <see cref="AuthorizationPolicy"/>. 5 /// </summary> 6 /// <param name="requirements"> 7 /// The list of <see cref="IAuthorizationRequirement"/>s which must succeed for 8 /// this policy to be successful. 9 /// </param> 10 /// <param name="authenticationSchemes"> 11 /// The authentication schemes the <paramref name="requirements"/> are evaluated against. 12 /// </param> 13 public AuthorizationPolicy(IEnumerable<IAuthorizationRequirement> requirements, IEnumerable<string> authenticationSchemes) 14 { 15 if (requirements == null) 16 { 17 throw new ArgumentNullException(nameof(requirements)); 18 } 19 20 if (authenticationSchemes == null) 21 { 22 throw new ArgumentNullException(nameof(authenticationSchemes)); 23 } 24 25 if (requirements.Count() == 0) 26 { 27 throw new InvalidOperationException(Resources.Exception_AuthorizationPolicyEmpty); 28 } 29 Requirements = new List<IAuthorizationRequirement>(requirements).AsReadOnly(); 30 AuthenticationSchemes = new List<string>(authenticationSchemes).AsReadOnly(); 31 } 32 33 34 public IReadOnlyList<IAuthorizationRequirement> Requirements { get; } 35 36 public IReadOnlyList<string> AuthenticationSchemes { get; } 37 38 public static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 39 { 40 ... 41 42 AuthorizationPolicyBuilder policyBuilder = null; 43 44 foreach (var authorizeDatum in authorizeData) 45 { 46 if (policyBuilder == null) 47 { 48 policyBuilder = new AuthorizationPolicyBuilder(); 49 } 50 51 var useDefaultPolicy = true; 52 if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy)) 53 { 54 var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy); 55 56 useDefaultPolicy = false; 57 } 58 59 var rolesSplit = authorizeDatum.Roles?.Split(','); 60 if (rolesSplit != null && rolesSplit.Any()) 61 { 62 var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); 63 policyBuilder.RequireRole(trimmedRolesSplit); 64 useDefaultPolicy = false; 65 } 66 67 var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(','); 68 if (authTypesSplit != null && authTypesSplit.Any()) 69 { 70 foreach (var authType in authTypesSplit) 71 { 72 if (!string.IsNullOrWhiteSpace(authType)) 73 { 74 policyBuilder.AuthenticationSchemes.Add(authType.Trim()); 75 } 76 } 77 } 78 79 if (useDefaultPolicy) 80 { 81 policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync()); 82 } 83 } 84 ... 85 86 return policyBuilder?.Build(); 87 } 88 }View Code
在类中定义有两个集合属性:Requirements和AuthenticationSchemes,分别用来存放授权规则IAuthorizationRequirement和认证方案名称。
在CombineAsync方法中,该方法会接受传入进来的IEnumerable<IAuthorizeData>集合,经过循环,把每个 IAuthorizeData中的三个属性进行了转换:
通过Policy名称在IAuthorizationPolicyProvider中查询返回AuthorizationPolicy,该对象中的集合属性将会被加入到AuthorizationPolicyBuilder中;
通过Roles在AuthorizationPolicyBuilder中使用RolesAuthorizationRequirement构造函数生成RolesAuthorizationRequirement对象并加入到Requirements集合,而AuthenticationSchemes则是加入到了AuthorizationPolicyBuilder对象中的AuthenticationSchemes集合
最后通过使用AuthorizationPolicyBuilder对象的Build方法将对象中的Requirements和AuthenticationSchemes作为AuthorizationPolicy构造函数的参数生成AuthorizationPolicy对象。由此可见CombineAsync即是完成将IEnumerable<IAuthorizeData>中的属性进行转换生成此次请求资源的完整授权策略。
而通过AuthorizationPolicy,也是顺利的将IAuthorizeData接口与IAuthorizationRequirement和AuthorizationHandler<TRequirement>联系起来了,也可以说是把用户添加在AuthorizeAttribute中的授权要求转换成了具体的AuthorizationRequirement对象,执行对象中的处理逻辑即可完成授权。
其实介绍完上述三个概念之后,我们的授权大致逻辑基本就清晰了:
1.通过请求资源匹配的Endpoint终结点获取到元数据中的IAuthorizeData;
2.将资源设置的IAuthorizeData中的三个属性经过查询和转换包装为一个整体的授权策略AuthorizationPolicy;
3.使用AuthorizationPolicy中的AuthenticationSchemes完成用户信息的认证;
4.使用AuthorizationPolicy中的Requirements完成用户授权
疑问
以上介绍了授权中非常重要的几个概念以及授权大致的基本逻辑,但是仔细思考,上述流程中还存在一些疑惑:
1.我们知道使用[Authorize(Roles ="admin")]基于角色授权,最终会根据名称生成RolesAuthorizationRequirement对象,那我们如何基于Policy进行授权呢?
如果需要使用Policy进行授权,我们一般需要在添加授权服务到容器的时候定义好PolicyName以及策略内容,例如下面的代码所示:
1 services.AddAuthorization(option => 2 { 3 option.AddPolicy("CustomPolicy", authorizationPolicyBuilder => 4 { 5 authorizationPolicyBuilder 6 .RequireRole("admin") 7 .RequireClaim(ClaimTypes.Email); 8 }); 9 });
然后在需要控制的资源上加上[Authorize(Policy = "CustomPolicy")] 即可。
2.通过AddAuthorization的委托参数设置AuthorizationOptions后,我们是如何在需要的时候获取到的呢?
AuthorizationOptions包含一个用于存储PolicyName和Policy内容关系的字典集合IDictionary<string, AuthorizationPolicy>,AddPolicy负责往字典中添加对应关系后,通过Configure选项模式保存AuthorizationOptions的参数配置到容器中,等到需要获取的时候通过IOptions即可获取到对象信息,使用PollicyName即可从字典集合中获取对应的AuthorizationPolicy。这也解释了在AuthorizationPolicy.CombineAsync方法中,我们通过PolicyName在IAuthorizationPolicyProvider中获取AuthorizationPolicy时,IAuthorizationPolicyProvider中的这个对象又是怎么来的,其实IAuthorizationPolicyProvider的实现中就是通过选项模式获取到AuthorizationOptions对象的。
3.系统在进入用户授权环节之前已经经过了使用默认的认证方案进行用户认证,为什么在AuthorizeAttribute这个授权特性中还会存在AuthenticationSchemes呢?也就是说最终转换到AuthorizationPolicy中的AuthenticationSchemes集合有什么用处呢?
在往容器中添加用户认证服务的时候,我们一般需要指定默认的认证方案,而我们的认证服务往往是可以添加多个认证方案的,在某些场景下,需要限定请求的资源在用户授权时使用非默认方案进行认证时,这个时候特性中的AuthenticationSchemes就起到了作用,开发人员能够通过灵活的指定AuthenticationSchemes属性,限制资源的证方案然后进行授权。
写在最后
这次关于Authorization的分享就到这里,考虑了很久应该用怎样的方式来进行本次分享,最终还是经过梳理要点后的这种方式可能会比较清晰,希望对大家有所收获,还有问题的朋友可以评论区留言讨论哈。新人博主,喜欢的话请大家点个赞,也非常欢迎大家提出宝贵的意见!!!