一、前言
接上一篇《asp.net core 3.x 授权中的概念》,本篇看看asp.net core默认授权的流程。从两个方面来看整个授权系统是怎么运行的:启动阶段的配置、请求阶段中间件的处理流程。
由于asp.net core 3.x目前使用终结点路由,因此授权框架可以用于所有asp.net web项目类型,比如:webapi mvc razorpages...。但本篇只以MVC为例
二、核心概念关系图
三、启动阶段的配置
主要体现为3点
- 注册相关服务
- 配置授权选项对象AuthorizationOptions
- 注册授权中间件
3.1、注册相关服务和选项配置
在mvc项目Startup.ConfigreServices中services.AddControllersWithViews(); (MvcServiceCollectionExtensions)用来向依赖注入框架注册各种mvc相关服务。其中会调用services.AddAuthorization(选项)扩展方法(PolicyServiceCollectionExtensions)注册授权相关服务,此扩展方法内部还会调用两个扩展方法,这里不再深入。
这里主要需要搞懂2个问题:
- 方法传入的选项
- 具体注册了哪些服务
3.1.1、授权选项AuthorizationOptions
AddAuthorization扩展方法的参数是Action<AuthorizationOptions>类型的,这是asp.net core中典型的选项模型,将来某个地方需要时,直接注入此选项对象,那时依赖注入容器会使用此委托对这个选项对象赋值。所以我们在启动时可以通过此对象来对授权框架进行配置。
最最重要的是我们可以在这里配置全局授权策略列表,参考上图的右侧中间部分,源码不多,注意注释。
1 //代表授权系统的全局选项对象,里面最最核心的就是存储着全局授权策略 2 public class AuthorizationOptions 3 { 4 //存储全局授权策略(AuthorizationPolicy),key是策略唯一名,方便将来获取 5 private IDictionary<string, AuthorizationPolicy> PolicyMap { get; } = new Dictionary<string, AuthorizationPolicy>(StringComparer.OrdinalIgnoreCase); 6 //授权验证时,将遍历所有授权处理器(Authorization)逐个进行验证,若某个发生错误是否立即终止后续的授权处理器的执行 7 public bool InvokeHandlersAfterFailure { get; set; } = true; 8 //默认授权策略,拒绝匿名访问 9 public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); 10 //若将来授权检查时没有找到合适的授权策略,默认授权策略也是空的情况下会回退使用此策略 11 public AuthorizationPolicy FallbackPolicy { get; set; } 12 //添加全局策略 13 public void AddPolicy(string name, AuthorizationPolicy policy) 14 { 15 if (name == null) 16 { 17 throw new ArgumentNullException(nameof(name)); 18 } 19 20 if (policy == null) 21 { 22 throw new ArgumentNullException(nameof(policy)); 23 } 24 25 PolicyMap[name] = policy; 26 } 27 //添加全局策略,同时可以对此策略进行配置 28 public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy) 29 { 30 if (name == null) 31 { 32 throw new ArgumentNullException(nameof(name)); 33 } 34 35 if (configurePolicy == null) 36 { 37 throw new ArgumentNullException(nameof(configurePolicy)); 38 } 39 40 var policyBuilder = new AuthorizationPolicyBuilder(); 41 configurePolicy(policyBuilder); 42 PolicyMap[name] = policyBuilder.Build(); 43 } 44 //获取指定名称的策略 45 public AuthorizationPolicy GetPolicy(string name) 46 { 47 if (name == null) 48 { 49 throw new ArgumentNullException(nameof(name)); 50 } 51 52 return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null; 53 } 54 }
举个栗子:
1 services.AddControllersWithViews(); 2 services.AddRazorPages(); 3 services.AddAuthorization(opt => 4 { 5 opt.AddPolicy("授权策略1", builer => { 6 builer.RequireRole("admin", "manager"); 7 builer.AddAuthenticationSchemes("cookie", "google"); 8 //继续配置.... 9 }); 10 opt.AddPolicy("授权策略2", builer => { 11 builer.RequireRole("teacher"); 12 builer.AddAuthenticationSchemes("wechat", "qq"); 13 //继续配置.... 14 }); 15 });
3.1.2、具体注册了哪些服务:
- 策略评估器IPolicyEvaluator:名字有点诡异。默认实现PolicyEvaluator,授权中间件委托它来实现身份验证和授权处理,它内部会调用AuthorizationService,进而执行所有授权处理器AuthorizationHandler
- 授权服务IAuthorizationService:上一篇有说,不详述了,默认实现DefaultAuthorizationService,除了授权中间件会调用它来进行授权处理,我们业务代码中也可以调用它来做授权验证,比如:针对资源的特殊授权
- 授权策略提供器IAuthorizationPolicyProvider:默认实现DefaultAuthorizationPolicyProvider,可以通过它来获取指定名称的授权,它就是从全局授权策略列表里去找,也就是上面说的AuthorizationOptions中
- 授权处理器提供器IAuthorizationHandlerProvider:默认实现DefaultAuthorizationHandlerProvider,通过它来获取系统中所有的授权处理器,其实就是从IOC容器中获取
- 授权评估器IAuthorizationEvaluator:默认实现DefaultAuthorizationEvaluator,授权处理器AuthorizationHandler在执行完授权后,结果是存储在AuthorizationHandlerContext中的,这里的评估器只是根据AuthorizationHandlerContext创建一个授权结果AuthorizationResult,给了我们一个机会来加入自己的代码而已
- 授权处理器上下文对象的工厂IAuthorizationHandlerContextFactory:默认实现DefaultAuthorizationHandlerContextFactory,授权处理器AuthorizationHandler在授权时需要传入AuthorizationHandlerContext(上面说了授权完成后的结果也存储在里面)。所以在执行授权处理器之前需要构建这个上下文对象,就是通过这个工厂构建的,主要的数据来源就是 当前 或者 指定的 授权策略AuthorizationPolicy
- 授权处理器IAuthorizationHandler:默认实现PassThroughAuthorizationHandler。授权的主要逻辑在授权处理器中定义,授权服务在做授权时会遍历系统所有的授权处理器逐一验证,而验证往往需要用到授权依据,PassThroughAuthorizationHandler比较特殊,它会看授权依据是否已经实现了IAuthorizationHandler,如过是,则直接把授权依据作为授权处理器进行执行。因为多数情况下一个授权处理器类型是专门针对某种授权依据定义的。
这些接口都是扩展点,就问你怕不怕?当然绝大部分时候我们不用管,默认的就足够用了。
3.2、注册授权中间件
主要注意的位置的为题,必须在路由和身份验证之后。
1 app.UseRouting(); 2 app.UseAuthentication(); 3 app.UseAuthorization();
扩展方法内部注册了AuthorizationMiddleware
四、请求阶段的处理流程
如果你对mvc稍有经验,就晓得在一个Action上使用[Authorize]就可以实施授权,现在我们假设我们在默认mvc项目中的HomeController定义如下Action,并应用授权标签
1 [Authorize(Policy = "p1")]//使用全局授权策略中的"p1"进行授权判断 2 [Authorize(AuthenticationSchemes = "google")]//只允许使用google身份验证登录的用户访问 3 [Authorize(Roles = "manager")]//只允许角色为manager的访问 4 public IActionResult Privacy() 5 { 6 return View(); 7 }
4.1、授权中间件AuthorizationMiddleware
核心步骤如下:
- 从当前请求拿到终结点
- 通过终结点拿到其关联的IAuthorizeData集合
- 调用AuthorizationPolicy.CombineAsync根据IAuthorizeData集合创建一个复合型策略,此策略就是本次用来做授权检查的策略,也就是文章中多次提到的当前这略
- 从IOC容器中获取策略评估器对上面得到的策略进行身份验证,多种身份验证得到的用户证件信息会合并进HttpContext.User
- 若Action上应用了IAllowAnonymous,则放弃授权检查(为毛不早点做这步?)
- 通过策略评估器对策略进行授权检查,注意这里的参数,传入身份验证评估结果和将终结点作为资源
- 若授权评估要求质询,则遍历策略所有的身份验证方案,进行质询,若策略里木有身份验证方案则使用默认身份验证方案进行质询
- 若授权评估拒绝就直接调用身份验证方案进行拒绝
步骤1、2得益于asp.net core 3.x的终结点路由,我们可以在进入MVC框架前就拿到Action及其之上应用的各种Atrribute,从而得到我们对当前授权策略定制所需要的数据
步骤3会根据得到IAuthorizeData集合(AuthorizeAttribute实现了IAuthorizeData,它不再是一个过滤器)创建当前准备用来做授权的策略。授权策略中 “身份验证方案列表” 和 “授权依据列表”,就是通过这里的标签来的。具体来说:
- [Authorize(Policy = "p1")]:会通过“p1”去全局授权策略(AuthorizationOptions对象中)拿到对应的策略,然后与当前策略合并,也就是把“p1”策略中的身份验证方案列表、授权依据列表与当前策略合并。
- [Authorize(AuthenticationSchemes = "google")]:用来定制当前策略的“身份验证方案列表”,当然最终和上面说的合并,
- [Authorize(Roles = "manager")]:会创建一个RolesAuthorizationRequirement类型的授权依据加入到当前策略中
这些Attribute可以应用多次,最终都是来定制当前授权策略的。后续步骤都会依赖此授权策略。
步骤4中,若发现本次授权策略中定义了多个身份验证方案,则会注意进行身份验证,得到的多张证件会合并到当前用户HttpContext.User中,当然默认身份验证得到的用户信息也在其中。
上面步骤4、6是委托策略评估器PolicyEvaluator来完成的,往下看..
4.2、策略评估器PolicyEvaluator
核心任务就两个,身份验证、进行授权
4.2.1、AuthenticateAsync
若策略没有设置AuthenticationSchemes,则只判断下当前请求是否已做身份验证,若做了就返回成功
若策略设置了AuthenticationSchemes,则遍历身份验证方案逐个进行身份验证处理 context.AuthenticateAsync(scheme); ,将所有得到的用户标识重组成一个复合的用户标识。
4.2.2、AuthorizeAsync
调用IAuthorizationService进行权限判断,若成功则返回成功。
否则 若身份验证通过则 PolicyAuthorizationResult.Forbid() 直接通知身份验证方案,做拒绝访问处理;否则返回质询
所以授权检查的任务又交给了授权服务AuthorizationService
4.3、授权服务AuthorizationService
核心步骤如下:
- 通过IAuthorizationHandlerContextFactory创建AuthorizationHandlerContext,此上下文包含:授权依据(来源与当前授权策略) 当前用户(httpContext.User)和资源(当前终结点)
- 遍历所有授权处理器AuthorizationHandler,这些授权处理器是通过IAuthorizationHandlerProvider获取的,默认情况下是从IOC容器中获取的。逐个调用每个授权处理器执行授权检查
- 所有授权处理器执行验证后的结果还是存储在上面说的这个上下文对象AuthorizationHandlerContext中。回调授权评估器IAuthorizationEvaluator将这个上下文对象转换为授权结果AuthorizationResult
步骤2还会判断AuthorizationOptios.InvokeHandlersAfterFailure,来决定当某个处理器发生错误时,是否停止执行后续的授权处理器。
4.4、授权处理器AuthorizationHandler
前面讲过,默认只注册了一个PassThroughAuthorizationHandler授权处理器,它会遍历当前授权策略中实现了IAuthorizationHandler的授权依据(意思说这些对象既是授权处理器,也是授权依据)。直接执行它们。
public class PassThroughAuthorizationHandler : IAuthorizationHandler { public async Task HandleAsync(AuthorizationHandlerContext context) { foreach (var handler in context.Requirements.OfType<IAuthorizationHandler>()) { await handler.HandleAsync(context); } } }
以基于角色的授权依据RolesAuthorizationRequirement为例,它继承于AuthorizationHandler<RolesAuthorizationRequirement>,且实现IAuthorizationRequirement
1 public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement 2 { 3 //省略部分代码... 4 public IEnumerable<string> AllowedRoles { get; } 5 protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) 6 { 7 if (context.User != null) 8 { 9 bool found = false; 10 if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any()) 11 { 12 // Review: What do we want to do here? No roles requested is auto success? 13 } 14 else 15 { 16 found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r)); 17 } 18 if (found) 19 { 20 context.Succeed(requirement); 21 } 22 } 23 return Task.CompletedTask; 24 } 25 }
五、最后
可以感受到asp.net core 3.x目前的权限设计棒棒哒,默认的处理方式已经能满足大部分需求,即使有特殊需求扩展起来也非常简单,前面注册部分看到注册了各种服务,且都有默认实现,这些服务在授权检查的不同阶段被使用,如果有必要我们可以自定义实现某些接口来实现扩展。本篇按默认流程走了一波,最好先看前一篇。这时候再去看官方文档或源码应该更容易。日常开发使用其实参考官方文档就足够了,无非就是功能权限和数据权限,看情况也许不会写后续的应用或扩展篇了。