预计是通过三篇来将清楚asp.net core 3.x中的授权:1、基本概念介绍;2、asp.net core 3.x中授权的默认流程;3、扩展。
在完全没有概念的情况下无论是看官方文档还是源码都晕乎乎的,希望本文能帮到你。不过我也是看源码结合官方文档看的,可能有些地方理解不对,所以只作为参考。
要求对asp.net core的基础有所有了解:Host、依赖注入、日志、配置、选项模式、终结点路由、身份验证等。还是推荐A大博客
归纳来说授权就是:某人 针对某个资源 可以做什么操作,如:张三 针对销售订单 可以查看、审核、取消等操作
除了上面这3个概念外加一个权限判断逻辑,就组成了授权系统。下面逐一介绍asp.net core 3.x中授权系统涉及到的相关概念
注:
功能权限:这是我们通常说的某人(包含所属角色,所属部门等)可以访问某个菜单下的某个按钮
数据权限:上面说的订单金额大于10000的必须要经理角色才可以审核
这个说来也没啥区别,一个按钮通常是对应到mvc中的某个action,所以还是可以看成是操作;金额大于10000,这个也只是对资源的一种选定
还有一种情况,说一个按钮的点击对应到一个action,那么这个按钮到底是看做“操作”呢,还是把这个Action看成是一个页面地址,作为资源呢?这个就看怎么设计了,mvc默认是当做资源。
现实生活中,一个人有多种证件,比如身份证、登机牌、就诊卡等,你去到不同的机构需要出示不同的证件,而每张证件上又有不同的信息,比如身份验证上有身份证号、姓名、性别、出示日期等等... 登机牌上有 航班号、座位号之类的。
在asp.net core中,ClaimsPrincipal就代表上面说的这个人,它可能存在多张证件,证件用ClaimsIdentity表示,当然得有一张证件作为主要证件(如身份证);一张证件又包含多条信息,可以用类似字典的形式IDictionary<string,string>来存储证件的信息,但是字典不够面向对象,所以单独为证件上的一条信息定义了一个类Claim,拿身份证上的出生日期来说,ClaimType="出生日期",Value=“1995-2-4”
上面我们一直拿一个人拥有多张证件来举例,其实并不准确,因为对系统来说并不关心是谁登录,可能是一个用户、也可能是一个第三方应用。所以将ClaimsPrincipal理解为一个登录到系统的主体更合理。
在一个系统中可能同时存在多种身份验证方案,比如我们系统本身做了用户管理功能,使用最简单的cookie身份验证方案,或者使用第三方登录,微信、QQ、支付宝账号登录,通常一个身份验证方案可以产生一张证件(ClaimsIdentity),当然某个身份验证方案也可以将获得的Claim添加到一张现有的证件中,这个是灵活的。默认情况下,用户登录时asp.net core会选择设置好的默认身份验证方案做身份验证,本质是创建一个ClaimsPrincipal,并根据当前请求创建一个证件(ClaimsIdentity),然后将此ClaimsIdentity加入到ClaimsPrincipal,最后将这个ClaimsPrincipal设置到HttpContext.User属性上。身份验证不是本篇重点,详细描述参考:《asp.net core 3.x 身份验证-1涉及到的概念》。我们目前只要记住一个字符串代表一个身份验证方案,它可以从当前请求或第三方去获得一张证件(ClaimsIdentity)
当用户登录后,我们已经可以从HttpContext.User拿到当前用户,里面就包含一张或多张证件,后续的权限判断通常就依赖里面的信息,比如所属角色、所属部门,除了证件的信息我们也可以通过用户id去数据库中查询得到更多用户信息作为权限判断的依据。
资源的概念很宽泛,上面说的销售订单、客户档案、属于资源,我们可以控制某个用户是否能查看、新增、审核订单。或者说一个页面也是一种资源,我们希望控制某用户是否能访问某个页面。在asp.net core中直接以object类型来表示资源,因为asp.net core作为一个框架,它不知道将来使用此框架的开发者到底是对什么类型的资源做权限限制。
在我们日常开发中经常在Action上应用AuthorizeAttribute标签来进行授权控制,其实这里就是将这个Action当做资源。由于目前asp.net core 3.x中默认使用终结点路由,所以现在在asp.net core 3.x中的默认授权流程中当前Endpoint就是资源
记住权限判断中不一定需要资源的参与,比如只要用户登录,就允许使用系统中所有功能。此时整个系统就是资源,允许所有操作。
试想这样一种权限需求:要求属于角色"经理"或"系统管理员"的用户可以访问系统任何功能。当我们做权限判断时我们可以从HttpContext.User得到当前用户,从其证件列表中总能找到当前用户的所属角色,那么这里需要进行比较的两个角色"经理"、"系统管理员"从哪里获得呢?
再比如:要求只要当前用户的证件中包含一个"特别通行证"的Calim,就允许他访问系统的任何功能。同上面的情况一样,在判断权限时我们可以知道当前登录用户的Calim列表,那需要进行比对的"特别通行证"这个字符串从哪来呢?
asp.net core将这种权限判断时需要用来比对的数据定义为IAuthorizationRequirement,我这里叫做"授权依据",在一次权限判断中可能会存在多个判断,所以可能需要多个授权依据,文件后面会讲如何定制授权依据
其实某种意义上说“当前用户(及其包含的Calim列表)”也可以看做是一种依据,因为它也是在授权判断过程中需要访问的数据,但是这个我们是直接通过HttpContext.User来获取的,不需要我们来定义。
当我们针对某个页面或Action进行授权时可以直接从当前路由数据中获取Action名,在asp.net core 3.x中甚至更方便,可以在请求管道的早期就能获得当前请求的终结点。所以针对Action的访问也不需要定义成授权依据中
所以授权依据算是一种静态数据,为了更好的理解,下面列出asp.net core中已提供的几种授权依据
public string ClaimType { get; } public IEnumerable<string> AllowedValues { get; }
将来在权限判断是会判断当前用户的Claim列表中是否包含一个类型为ClaimType的Claim,若AllowedValues有数据,则进一步判断是否完整包含AllowedValues中定义的值
策略同时作为身份验证方案和授权依据的容器,它包含本次授权需要的数据。
请求抵达时asp.net core会找到默认身份验证方案进行身份验证(根据请求获取用户ClaimsPrincipal),但有时候我们希望由自己来指定本次授权使用哪些身份验证验证方案,而不是使用默认的,这样将来身份验证过程中会调用设置的这几个身份验证方案去获得多张证件,此时HttpContext.User中就包含多张证件。所以授权策略里包含多种身份验证方案。
一次授权可能需要多种判断,比如同时判断所属角色、并且是否包含哪种类型的Calim并且.....,某些判断可能需要对比“授权依据”,所以一个授权策略包含多个授权依据。
另外我们可以将多个授权策略合并成一个对吗?所有的身份验证方案合并,所有的“授权依据”合并
将来授权检查时将根据身份验证方案获取当前用户的多个证件(里面包含很多Cliam可以用作权限判断),然后逐个判断授权依据,若都满足则认为授权检查成功。
若是针对某个资源的授权,授权方法大概是这样定义的xxxx.Authorize(策略,订单),这里不一定直接传入整个订单,可能只传入订单金额,这个根据业务需要。若是简单的情况只判断页面访问权限,则xxx.Authorize(策略),因为当前页面可以直接通过当前请求获取。
在asp.net core 3.x中启动阶段我们可以定义一个授权策略列表,这个看成是全局授权策略,一直存在于应用中。
在应用运行时,每次进行授权时会动态创建一个授权策略,这个策略是最终进行本次授权检查用的,它可能会引用某一个或多个全局策略,所谓的引用就是合并其“身份验证方案”列表和“授权依据列表”,当然其自身的“身份验证方案”列表和“授权依据列表”也是可以定制的,待会在AuthorizeAttribute部分再详细说
主要用来帮助创建一个授权策略(.net中典型的Builder模式),使用步骤是:
下面用伪代码感受下
var builder = new AuthorizationPolicyBuilder(); builder.RequireRole("Manager","Admin"); //builder....继续配置 var authorizationPolicy = builder.Build();
RequireRole将为最终会生成的策略中的“授权依据”列表加入一个RolesAuthorizationRequirement("Manager","Admin")。其它类似的api就不介绍了。
上面说的当前用户、授权依据、以及授权时传递进来的资源都是可以看成是静态的数据,作为授权判断的依据,真正授权的逻辑就是用IAuthorizationHandler来表示的,先看看它的定义
public interface IAuthorizationHandler { Task HandleAsync(AuthorizationHandlerContext context); }
中包含当前用户、授权依据列表和参与授权判断的资源,前者是根据授权策略中的多个身份验证方案经过身份验证后得到的;后者就是授权策略中的授权依据列表。在方法内部处理成功或失败的结果是直接存储到context对象上的。
一个应用中可以存在多个AuthorizationHandler,在执行授权检查时逐个调用它们进行检查,若都成功则本次授权成功。
上面聊过授权依据是有多种类型的,将来还可能自定义,通常授权依据不同,授权的判断逻辑也不同。
所以这样看来一种“授权依据”类型应该对应一种“授权处理器”,所以微软定义了public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler ,这个TRequirement就代表这个授权处理器类型是针对哪种类型的“授权依据的”
一个授权策略AuthorizationPolicy是包含多个“授权依据”的,这其中可能有几个“授权依据”的类型是一样的,只是里面存储的值不同,以OperationAuthorizationRequirement为例,一个授权策略里可能包含如下授权依据列表:
new OperationAuthorizationRequirement{ Name="新增" } new OperationAuthorizationRequirement{ Name="审核" } new RolesAuthorizationRequirement("Manager","Admin"); //其它。。。
所以一个授权处理器AuthorizationHandler虽然只关联一种类型“授权依据”,但是一个授权处理器实例可以处理多个相同类型的“授权依据”
在授权过程中,每个AuthorizationHandler<TRequirement>会找到自己能处理的“授权依据”,逐个进行检查
定义是这样的 public abstract class AuthorizationHandler<TRequirement, TResource> : IAuthorizationHandler
跟AuthorizationHandler<TRequirement>定义及处理逻辑唯一的区别是多了个TResource,在授权过程中是可以对给定资源进行判断的,资源在AuthorizationHandlerContext.Resource,这个是object类型,为了方便子类降重重写,所以由这里的父类将AuthorizationHandlerContext.Resource转换为TResource
干脆贴下源码吧
1 public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler 2 where TRequirement : IAuthorizationRequirement 3 { 4 public virtual async Task HandleAsync(AuthorizationHandlerContext context) 5 { 6 foreach (var req in context.Requirements.OfType<TRequirement>()) 7 { 8 await HandleRequirementAsync(context, req); 9 } 10 } 11 12 protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement); 13 } 14 15 public abstract class AuthorizationHandler<TRequirement, TResource> : IAuthorizationHandler 16 where TRequirement : IAuthorizationRequirement 17 { 18 public virtual async Task HandleAsync(AuthorizationHandlerContext context) 19 { 20 if (context.Resource is TResource) 21 { 22 foreach (var req in context.Requirements.OfType<TRequirement>()) 23 { 24 await HandleRequirementAsync(context, req, (TResource)context.Resource); 25 } 26 } 27 } 28 29 protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TResource resource); 30 }
我们发现通常一个授权依据的类型会有个对应的授权处理器,如果只定义一个类,实现这两种接口事情不是更简单吗?举个例子:
1 public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement 2 { 3 public RolesAuthorizationRequirement(IEnumerable<string> allowedRoles) 4 { 5 AllowedRoles = allowedRoles; 6 } 7 public IEnumerable<string> AllowedRoles { get; } 8 protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) 9 { 10 if (context.User != null) 11 { 12 bool found = false; 13 if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any()) 14 { 15 // Review: What do we want to do here? No roles requested is auto success? 16 } 17 else 18 { 19 found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r)); 20 } 21 if (found) 22 { 23 context.Succeed(requirement); 24 } 25 } 26 return Task.CompletedTask; 27 } 28 }
我们上面讲的微软定义的那几个授权依据基本都是这样定义的
如果是用的asp.net core 3.x之前的版本,那么在Action执行前做授权判断比较合适,常用的就是过滤器Filter咯。这个我不是特别确定,至少在.net framework时代是用的授权过滤器AuthorizeAttribute
请求 > 其它中间件 > 路由中间件 > 身份验证中间件 > MVC中间件 > Controller > [授权过滤器]Action
若是asp.net core 3.x之后,由于目前用的终结点路由,所以在 路由中间件 和 身份验证中间件 后做权限判断(使用授权中间件)比较合适,因为 路由中间件执行后我们可以从当前请求上下文中获取当前终结点(它代表一个Action或一个页面)。身份验证中间件执行后可以通过HttpContext.User获取当前用户,此时有了访问的页面和当前用户 就可以做权限判断了
请求 > 其它中间件 > 路由中间件(这里就拿到终结点了) > 身份验证中间件 > 授权中间件 > MVC中间件 > Controller > Action
还有一种情况是在业务代码内部去执行权限判断,比如:希望销售订单金额大于10000的,必须要经理角色才可以审核,此时因为我们要先获取订单才知道它的金额,所以我们最好在Action执行内部根据路由拿到订单号,去数据库查询订单金额后,调用某个方法执行权限判断。
所以执行权限判断的点不同,AuthorizationService就是用来封装授权检查的,我们在不同的点都可以来调用它执行权限判断。看看接口定义
public interface IAuthorizationService { Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements); Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); }
user:要进行判断的用户,它里面可能存在一张或多张证件
resource:可能是一个终结点,也可能是一个页面RazorPage,也可能是一个订单(或者是单独的订单金额)
requirements:授权依据列表
policyName:一个授权策略名
在授权中间件和在业务逻辑代码中手动进行授权检查时都是调用此接口
它内部会去调用AuthorizationHandler来进行权限判断。
在asp.net core 3.x中 启动阶段可以配置一个全局策略列表,它一直存在于系统中,暂时称为“静态策略列表”
在每次执行授权检查时也需要一个策略,暂时称为“运行时授权策略”,授权中间件执行时就会创建此策略,然后调用AuthorizationService根据此策略进行权限判断,那此策略中的“授权依据”和“身份验证方案”这俩列表从哪来的呢?就是在Action通过AuthorizeAttribute来定制的,它实现 IAuthorizeData接口
如果你对早期版本mvc有一丢丢了解的话,你可能记得有个授权过滤器的概念AuthorizeAttribute,在Action执行前会先去做授权判断,若成功才会继续执行Action,否则就返回403.
在asp.net core 3.x中不是这样了,AuthorizeAttribute只是用来定制当前授权策略(AuthorizationPolicy)的,并不是过滤器,它实现IAuthorizeData接口,此接口定义如下:
public interface IAuthorizeData { string Policy { get; set; }//直接指定此Action将来授权验证时要使用的授权策略AuthorizationPolicy,此策略会被合并到当前授权策略 string Roles { get; set; } //它会创建一个基于角色的授权依据RolesAuthorizationRequirement,此依据会被放入当前授权策略 string AuthenticationSchemes { get; set; }//它用来定义当前授权策略里要使用哪些身份验证方案 }
Policy属性指明从“静态策略列表”拿到指定策略,然后将其“授权依据”和“身份验证方案”这俩列表合并到“运行时授权策略”
看个例子:
1 [Authorize(AuthenticationSchemes = "cookie,jwtBearer")] 2 [Authorize(Roles = "manager,admin")] 3 [Authorize(policy:"test")] 4 [Authorize] 5 public IActionResult Privacy() 6 { 7 return View(); 8 }
以上定制只是针对使用授权中间件来做权限判断时,对当前授权策略进行定制。若我们直接在业务代码中调用AuthorizationService手动进行权限判断呢,就截止调用咯。参考上面的描述
上面我们介绍了何时实施授权检查,授权中间件(AuthorizationMiddleware)就是其中最为常用的一个授权检查点,相当于是一个授权检查的入口方法,它在进入MVC中间件之前就可以做授权判断,所以比之前的在Action上做判断更早。并且由于授权检查是根据终结点的,因此同一套授权代码可以应用在mvc/webapi/razorPages...等多种web框架。由于授权检查依赖当前访问的终结点(若不理解终结点,可以暂时认为它=Action及其之上应用的各种Attribute) 和 当前登录用户,因此 授权中间件 应该在 路由中间件 和 身份验证中间件 之后注册
1 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
2 {
3 //略....
4 app.UseHttpsRedirection();
5 app.UseStaticFiles();
6 app.UseRouting();
7 app.UseAuthentication();
8 app.UseAuthorization();
9 app.UseEndpoints(endpoints =>
10 {
11 endpoints.MapRazorPages();
12 });
13 }
它的核心步骤大致如下:
所以重点是可以在执行mvc中间件之前拿到终结点及其之上定义的AuthorizeAttribute,从其中的数据就可以构建出本次权限判断的“授权策略”,有了授权策略就可以通过AuthorizationService执行授权判断,内部会使用到授权处理器AuthorizationHandler
暂时就BB到这里,先有个大概印象,下一篇按asp.net core的默认授权流程走走源码,再结合此篇应该就差不多了...
联系客服