ASP.NET Core MVC 授权的扩展:自定义 Authorize 和 IApplicationModelProvide

一、概述

ASP.NET Core MVC 提供了基于角色( Role )、声明( Chaim ) 和策略 ( Policy ) 等的授权方式。在实际应用中,可能采用部门( Department , 本文采用用户组 Group )、职位 ( 可继续沿用 Role )、权限( Permission )的方式进行授权。要达到这个目的,仅仅通过自定义 IAuthorizationPolicyProvider 是不行的。本文通过自定义 IApplicationModelProvide 进行扩展。


二、PermissionAuthorizeAttribute : IPermissionAuthorizeData

AuthorizeAttribute 类实现了 IAuthorizeData 接口:


namespace Microsoft.AspNetCore.Authorization
{
 /// <summary>
 /// Defines the set of data required to apply authorization rules to a resource.
 /// </summary>
 public interface IAuthorizeData
 {
  /// <summary>
  /// Gets or sets the policy name that determines access to the resource.
  /// </summary>
  string Policy { get; set; }
  /// <summary>
  /// Gets or sets a comma delimited list of roles that are allowed to access the resource.
  /// </summary>
  string Roles { get; set; }
  /// <summary>
  /// Gets or sets a comma delimited list of schemes from which user information is constructed.
  /// </summary>
  string AuthenticationSchemes { get; set; }
 }
}

使用 AuthorizeAttribute 不外乎如下几种形式:


[Authorize]
[Authorize("SomePolicy")]
[Authorize(Roles = "角色1,角色2")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

当然,参数还可以组合起来。另外,Roles 和 AuthenticationSchemes 的值以半角逗号分隔,是 Or 的关系;多个 Authorize 是 And 的关系;Policy 、Roles 和 AuthenticationSchemes 如果同时使用,也是 And 的关系。

如果要扩展 AuthorizeAttribute,先扩展 IAuthorizeData 增加新的属性:


public interface IPermissionAuthorizeData : IAuthorizeData
{
   string Groups { get; set; }
   string Permissions { get; set; }
}

然后定义 AuthorizeAttribute:


[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class PermissionAuthorizeAttribute : Attribute, IPermissionAuthorizeData
{
   public string Policy { get; set; }
   public string Roles { get; set; }
   public string AuthenticationSchemes { get; set; }
   public string Groups { get; set; }
   public string Permissions { get; set; }
}

现在,在 Controller 或 Action 上就可以这样使用了:


[PermissionAuthorize(Roles = "经理,副经理")] // 经理或部门经理
[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理"] // 研发部经理或生成部经理。Groups 和 Roles 是 `And` 的关系。
[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] // 研发部经理或生成部经理,并且有请假审批的权限。Groups 、Roles 和 Permission 是 `And` 的关系。

数据已经准备好,下一步就是怎么提取出来。通过扩展 AuthorizationApplicationModelProvider 来实现。

三、PermissionAuthorizationApplicationModelProvider : IApplicationModelProvider

AuthorizationApplicationModelProvider 类的作用是构造 AuthorizeFilter 对象放入 ControllerModel 或 ActionModel 的 Filters 属性中。具体过程是先提取 Controller 和 Action 实现了 IAuthorizeData 接口的 Attribute,如果使用的是默认的DefaultAuthorizationPolicyProvider,则会先创建一个 AuthorizationPolicy 对象作为 AuthorizeFilter 构造函数的参数。
创建 AuthorizationPolicy 对象是由 AuthorizationPolicy 的静态方法 public static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 来完成的。该静态方法会解析 IAuthorizeData 的数据,但不懂解析 IPermissionAuthorizeData

因为 AuthorizationApplicationModelProvider 类对 AuthorizationPolicy.CombineAsync 静态方法有依赖,这里不得不做一个类似的 PermissionAuthorizationApplicationModelProvider 类,在本类实现 CombineAsync 方法。暂且不论该方法放在本类是否合适的问题。


       public static AuthorizeFilter GetFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authData)
      {
          // The default policy provider will make the same policy for given input, so make it only once.
          // This will always execute synchronously.
          if (policyProvider.GetType() == typeof(DefaultAuthorizationPolicyProvider))
          {
              var policy = CombineAsync(policyProvider, authData).GetAwaiter().GetResult();
              return new AuthorizeFilter(policy);
          }
          else
          {
              return new AuthorizeFilter(policyProvider, authData);
          }
      }
      private static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
      {
          if (policyProvider == null)
          {
              throw new ArgumentNullException(nameof(policyProvider));
          }
          if (authorizeData == null)
          {
              throw new ArgumentNullException(nameof(authorizeData));
          }
          var policyBuilder = new AuthorizationPolicyBuilder();
          var any = false;
          foreach (var authorizeDatum in authorizeData)
          {
              any = true;
              var useDefaultPolicy = true;
              if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
              {
                  var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy);
                  if (policy == null)
                  {
                      //throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy));
                      throw new InvalidOperationException(nameof(authorizeDatum.Policy));
                  }
policyBuilder.Combine(policy);
                  useDefaultPolicy = false;
              }
              var rolesSplit = authorizeDatum.Roles?.Split(',');
              if (rolesSplit != null && rolesSplit.Any())
              {
                  var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
                  policyBuilder.RequireRole(trimmedRolesSplit);
                  useDefaultPolicy = false;
              }
              if(authorizeDatum is IPermissionAuthorizeData permissionAuthorizeDatum )
              {
                  var groupsSplit = permissionAuthorizeDatum.Groups?.Split(',');
                  if (groupsSplit != null && groupsSplit.Any())
                  {
                      var trimmedGroupsSplit = groupsSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
                      policyBuilder.RequireClaim("Group", trimmedGroupsSplit); // TODO: 注意硬编码
                      useDefaultPolicy = false;
                  }
                  var permissionsSplit = permissionAuthorizeDatum.Permissions?.Split(',');
                  if (permissionsSplit != null && permissionsSplit.Any())
                  {
                      var trimmedPermissionsSplit = permissionsSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
                      policyBuilder.RequireClaim("Permission", trimmedPermissionsSplit);// TODO: 注意硬编码
                      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;
      }

if(authorizeDatum is IPermissionAuthorizeData permissionAuthorizeDatum ) 为扩展部分。

四、Startup

注册 PermissionAuthorizationApplicationModelProvider 服务,需要在 AddMvc 之后替换掉 AuthorizationApplicationModelProvider 服务。


services.AddMvc();
services.Replac(ServiceDescriptor.Transient<IApplicationModelProvider,PermissionAuthorizationApplicationModelProvider>());

五、Jwt 示例


[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
   private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
   [HttpGet]
   [Route("SignIn")]
   public async Task<ActionResult<string>> SignIn()
   {
       var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
       {
           // 备注:Claim Type: Group 和 Permission 这里使用的是硬编码,应该定义为类似于 ClaimTypes.Role 的常量;另外,下列模拟数据不一定合逻辑。
           new Claim(ClaimTypes.Name, "Bob"),
           new Claim(ClaimTypes.Role, "经理"),  // 注意:不能使用逗号分隔来达到多个角色的目的,下同。
           new Claim(ClaimTypes.Role, "副经理"),
           new Claim("Group", "研发部"),
           new Claim("Group", "生产部"),
           new Claim("Permission", "请假审批"),
           new Claim("Permission", "权限1"),
           new Claim("Permission", "权限2"),
       }, JwtBearerDefaults.AuthenticationScheme));
       var token = new JwtSecurityToken(
           "SignalRAuthenticationSample",
           "SignalRAuthenticationSample",
           user.Claims,
           expires: DateTime.UtcNow.AddDays(30),
           signingCredentials: SignatureHelper.GenerateSigningCredentials("1234567890123456"));
       return _tokenHandler.WriteToken(token);
   }
   [HttpGet]
   [Route("Test")]
   [PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] // 研发部经理或生成部经理,并且有请假审批的权限。Groups 、Roles 和 Permission 是 `And` 的关系。
   public async Task<ActionResult<IEnumerable<string>>> Test()
   {
       var user = HttpContext.User;
       return new string[] { "value1", "value2" };
   }
}

六、问题

AuthorizeFilter 类显示实现了 IFilterFactory 接口的 CreateInstance 方法:


IFilterMetadata IFilterFactory.CreateInstance(IServiceProvider serviceProvider)
{
   if (Policy != null || PolicyProvider != null)
   {
       // The filter is fully constructed. Use the current instance to authorize.
       return this;
   }

   Debug.Assert(AuthorizeData != null);
   var policyProvider = serviceProvider.GetRequiredService<IAuthorizationPolicyProvider>();
   return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData);
}

竟然对 AuthorizationApplicationModelProvider.GetFilter 静态方法产生了依赖。庆幸的是,如果通过 AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 或 AuthorizeFilter(AuthorizationPolicy policy) 创建 AuthorizeFilter 对象不会产生什么不良影响。

七、下一步

[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] 这种形式还是不够灵活,哪怕用多个 Attribute, And 和 Or 的逻辑组合不一定能满足需求。可以在 IPermissionAuthorizeData 新增一个 Rule 属性,实现类似的效果:


[PermissionAuthorize(Rule = "(Groups:研发部,生产部)&&(Roles:请假审批||Permissions:超级权限)"]

通过 Rule 计算复杂的授权。

八、如果通过自定义 IAuthorizationPolicyProvider 实现?

另一种方式是自定义 IAuthorizationPolicyProvider ,不过还需要自定义 AuthorizeFilter。因为当不是使用 DefaultAuthorizationPolicyProvider 而是自定义 IAuthorizationPolicyProvider 时,AuthorizationApplicationModelProvider(或前文定义的 PermissionAuthorizationApplicationModelProvider)会使用 AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 创建 AuthorizeFilter 对象,而不是 AuthorizeFilter(AuthorizationPolicy policy)。这会造成 AuthorizeFilter 对象在 OnAuthorizationAsync 时会间接调用 AuthorizationPolicy.CombineAsync 静态方法。

这可以说是一个设计上的缺陷,不应该让 AuthorizationPolicy.CombineAsync 静态方法存在,哪怕提供个 IAuthorizationPolicyCombiner 也好。另外,上文提到的 AuthorizationApplicationModelProvider.GetFilter 静态方法同样不是一种好的设计。等微软想通吧。

参考资料

https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-2.1 

排版问题:http://blog.tubumu.com/2018/11/28/aspnetcore-mvc-extend-authorization/


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/318723.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

牛客题霸 [分糖果问题] C++题解/答案

牛客题霸 [分糖果问题] C题解/答案 题目描述 一群孩子做游戏&#xff0c;现在请你根据游戏得分来发糖果&#xff0c;要求如下&#xff1a; 每个孩子不管得分多少&#xff0c;起码分到一个糖果。任意两个相邻的孩子之间&#xff0c;得分较多的孩子必须拿多一些糖果。(若相同则…

不止代码 洛谷P1006 传纸条(dp)

传送门 走两次 dp[x1][y1][x2][y2]表示两条路分别到两个点的坐标后的最大值 为了防止走重&#xff0c;dp[x1][y1][x1][y1]赋值为无穷小 时间复杂度O&#xff08;n^4&#xff09; 代码 #include<cstdio> #include<cstring> #include<algorithm> #include<…

通俗易懂,C#如何安全、高效地玩转任何种类的内存之Span

前言作为.net程序员&#xff0c;使用过指针&#xff0c;写过不安全代码吗&#xff1f;为什么要使用指针&#xff0c;什么时候需要使用它&#xff1f;如果能很好地回答这两个问题&#xff0c;那么就能很好地理解今天了主题了。C#构建了一个托管世界&#xff0c;在这个世界里&…

P5180-[模板]支配树

正题 题目链接:https://www.luogu.com.cn/problem/P5180 题目大意 给出nnn个点的一张有向图&#xff0c;求每个点支配的点数量。 1≤n≤2105,1≤m≤31051\leq n\leq 2\times 10^5,1\leq m\leq 3\times 10^51≤n≤2105,1≤m≤3105 解题思路 首先定义半支配点semixsemi_xsemix…

P4062 [Code+#1]Yazid 的新生舞会(区间绝对众数+分治/树状数组维护高维前缀和)

P4062 [Code#1]Yazid 的新生舞会 杭电多校懂得都懂 Code1 分治 比较喜欢分治的做法&#xff0c;非常好写。skylee大佬题解 首先对于任何一个区间来说&#xff0c;由于两个端点不确定性非常难以一次性统计多组区间&#xff0c;因为它们没有相似之处。 考虑分治&#xff0c;…

牛客题霸 [判断回文] C++题解/答案

牛客题霸 [判断回文] C题解/答案 题目描述 给定一个字符串&#xff0c;请编写一个函数判断该字符串是否回文。如果回文请返回true&#xff0c;否则返回false。 题解&#xff1a; 左右两端同时向中间缩 代码&#xff1a; class Solution { public:/*** 代码中的类名、方法…

AC自动机:例题与机制详解

介绍 AC自动机是kmp算法和trie树的结合 大体就是做这样的题用&#xff1a; 可以发现&#xff0c;这题和trie树的区别是把多个单词往一篇文章匹配&#xff0c;而trie恰好相反 匹配的时候其实就是判断子串&#xff0c;所以又用到了kmp 定义失配指针nxt[i]&#xff1a;表示root到…

.NET Core实战项目之CMS 第七章 设计篇-用户权限极简设计全过程

写在前面这篇我们对用户权限进行极简设计并保留其扩展性。首先很感谢大家的阅读&#xff0c;前面六章我带着大家快速入门了ASP.NET Core、ASP.NET Core的启动过程源码解析及配置文件的加载过程源码解析并引入依赖注入的概念、Git的快速入门、Dapper的快速入门、Vue的快速入门。…

P4428-[BJOI2018]二进制【树状数组,set】

正题 题目链接:https://www.luogu.com.cn/problem/P4428 题目大意 长度为nnn的0/10/10/1串要求支持 修改一个位置求区间[l,r][l,r][l,r]有多少个子区间重排后的二进制数可以被三整除 1≤n≤1051\leq n\leq 10^51≤n≤105 解题思路 首先有22k%31(k∈Z)2^{2k}\%31(k\in Z)22k…

2021“MINIEYE杯”中国大学生算法设计超级联赛(5)Random Walk 2(推式子+矩阵逆+矩阵乘)

Random Walk 2 【2.4】Gauss-Jordan消元法求矩阵的逆 高斯消元求矩阵的逆&#xff0c;伴随单位矩阵一起消元即可。 [A,I]→[I,A−1][\text A,\text I]\to [\text I,\text A^{-1}][A,I]→[I,A−1] 移项变形&#xff0c;后就是个矩阵的逆&#xff0c;为啥赛时不写&#xff1f;&a…

牛客题霸 [顺时针旋转矩阵] C++题解/答案

牛客题霸 [顺时针旋转矩阵] C题解/答案 题目描述 有一个NxN整数矩阵&#xff0c;请编写一个算法&#xff0c;将矩阵顺时针旋转90度。 给定一个NxN的矩阵&#xff0c;和矩阵的阶数N,请返回旋转后的NxN矩阵,保证N小于等于300。 题解&#xff1a; 根据题目要求&#xff0c;先…

数论:扩展欧几里德(洛谷P1516 青蛙的约会)

欧几里德 基本思想&#xff1a;gcd(q,r)gcd(r,q%r); 证明&#xff0c;设q、r的最大公因数为a&#xff0c;则qxa&#xff0c;rya&#xff0c;xy互质 不妨设x>y&#xff08;显然如果小于会在一次gcd运算后交换&#xff09; 则q%r&#xff08;x%y&#xff09;*a 显然,其与r的最…

CF802O-April Fools‘ Problem(hard)【wqs二分,优先队列】

正题 题目链接:https://www.luogu.com.cn/problem/CF802O 题目大意 nnn天每条有aia_iai​和bib_ibi​。 每条可以花费aia_iai​准备至多一道题&#xff0c;可以花费bib_ibi​打印至多一道准备好了的题。 求准备kkk道题最少要花费多少。 1≤k≤n≤51051\leq k\leq n\leq 5\ti…

2021牛客暑期多校训练营3 G-Yu Ling(Ling YueZheng) and Colorful Tree(cdq分治)

G-Yu Ling(Ling YueZheng) and Colorful Tree HOWARLI题解 大致做法就是首先考虑哪些修改可能影响询问&#xff0c;当修改点权是询问的倍数时才可能影响询问。于是考虑把他们放在一起。 首先每次枚举每种询问的倍数&#xff0c;把这些修改和当前询问放在一起&#xff0c;由于…

参加胶东开发者技术大会有感

2015年的时候&#xff0c;也是在12月&#xff0c;我和Bob(https://www.cnblogs.com/nianming/)去北京参加了“全球架构师峰会”&#xff0c;在那次会议上&#xff0c;来自百度、腾讯、阿里巴巴、京东、美团、新浪微博、Twitter等公司的架构师、技术专家们分享了各自在架构方面的…

牛客题霸 [矩阵乘法] C++题解/答案

牛客题霸 [矩阵乘法] C题解/答案 题目描述 给定两个nn的矩阵A和B&#xff0c;求AB。 题解&#xff1a; 都学过矩阵相乘把&#xff0c;[i][k][i][j]*[j][k] 代码&#xff1a; class Solution { public:/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&…

专题:数列信息传递问题转化为图论合点问题(ybtoj-数列询问+序列破解)

文章目录前言&#xff1a;一、数列询问&#xff08;取模&#xff09;解析代码二、序列破解&#xff08;奇偶性&#xff09;解析代码thanks for reading&#xff01;前言&#xff1a; 在一个数列a中&#xff0c;对于一个大区间A和组成它的两个小区间a&#xff0c;b&#xff1b;…

CF786C-Till I Collapse【树状数组倍增,优先队列】

正题 题目链接:https://www.luogu.com.cn/problem/CF786C 题目大意 给出一个长度为nnn的序列。 对于每个k∈[1,n]k\in[1,n]k∈[1,n]求将nnn分成最少的段使得每段的长度不同。 1≤ai≤n≤1051\leq a_i\leq n\leq 10^51≤ai​≤n≤105 解题思路 考虑对于一个kkk我们的做法显然…

Docker最全教程——从理论到实战(五)

在笔者参加腾讯容器服务技术交流会时&#xff0c;我们了解到了藏区牧民的目前的生活艰辛状况&#xff0c;因此除了在同事朋友之间推荐其土特产之外&#xff0c;我们也在此进行初步分享&#xff0c;希望略尽绵薄之力&#xff0c;能够帮助到他们&#xff1a;货真价实、确保都是37…

2021牛客暑期多校训练营6 J-Defend Your Country(无向图点双+思维)

无向图联通分量 点u是割点&#xff0c;当且仅当 特判树根&#xff1a;u为树根&#xff0c;且u有多于1棵子树u不为树根&#xff0c;在递归树上u存在子节点v&#xff0c;满足&#xff1a;dfnu≤lowv\text{dfn}_u\leq \text{low}_vdfnu​≤lowv​ 如上图&#xff0c;v想走到u的组…