.Net Core实战之基于角色的访问控制的设计

前言

  上个月,我写了两篇微服务的文章:《.Net微服务实战之技术架构分层篇》与《.Net微服务实战之技术选型篇》,微服务系列原有三篇,当我憋第三篇的内容时候一直没有灵感,因此先打算放一放。

  本篇文章与源码原本打算实在去年的时候完成并发布的,然而我一直忙于公司项目的微服务的实施,所以该篇文章一拖再拖。如今我花了点时间整理了下代码,并以此篇文章描述整个实现思路,并开放了源码给予需要的人一些参考。

  源码:https://github.com/SkyChenSky/Sikiro.RBAC

RBAC

  Role-Based Access Contro翻译成中文就是基于角色的访问控制,文章以下我都用他的简称RBAC来描述。

  现信息系统的权限控制大多数采取RBAC的思想进行实现,其本质思想是对系统各种的操作权限不是直接授予具体的某个用户,而是在用户集合与权限集合之间建立一个角色,作为间接关联。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。

  通过以上的描述,我们可以分析出以下信息:

  •   用户与权限是通过角色间接关联的

  •   角色的本质就是权限组(权限集合)

  这样做的好处在于,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。

  

功能分析

权限分类

从权限的作用可以分为三种,功能权限、访问权限、数据权限

  • 功能权限

    • 功能权限指系统用户允许在页面进行按钮操作的权限。如果有权限则功能按钮展示,否则隐藏。

  • 访问权限

    • 访问权限指系统用户通过点击按钮后进行地址的请求访问的权限(地址跳转与接口请求),如果无权限访问,则由页面提示无权限访问。

  • 数据权限

    • 数据权限指用户可访问系统的数据权限,不同的用户可以访问不同的数据粒度。

数据权限的实现可大可小,大可大到对条件进行动态配置,小可小到只针对某个维度进行硬编码。不纳入这次的讨论范围。

用例图

非功能性需求

  时效性,直接影响到安全性,既然是权限控制,那么理应一修改权限后就立刻生效。曾经有同行问过我,是不是每一个请求都得去查一次数据库是否满足权限,如果是,数据库压力岂不是很大?

  安全性,每一个页面跳转,每一个读写请求都的进行一次权限验证,不满足的权限的功能按钮就不需要渲染,避免样式display:none的情况。

  开发效率,权限控制理应是框架层面的,因此尽可能作为非业务的侵入性,让开发人员保持原有的数据善增改查与页面渲染。

技术选型

LayUI

  学习门槛极低,开箱即用。其外在极简,却又不失饱满的内在,体积轻盈,组件丰盈,从核心代码到 API 的每一处细节都经过精心雕琢,非常适合界面的快速开发,它更多是为服务端程序员量身定做,无需涉足各种前端工具的复杂配置,只需面对浏览器本身,让一切你所需要的元素与交互,从这里信手拈来。作为国人的开源项目,完整的接口文档与Demo示例让入门者非常友好的上手,开箱即用的Api让学习成本尽可能的低,其易用性成为快速开发框架的基础。

MongoDB

  主要两大优势,无模式与横向扩展。对于权限模块来说,无需SQL来写复杂查询和报表,也不需要使用到多表的强事务,上面提到的时效性的数据库压力问题也可以通过分片解决。无模式使得开发人员无需预定义存储结构,结合MongoDB官方提供的驱动可以做到快速的开发。

数据库设计

 E-R图

 

  一个管理员可以拥有多个角色,因此管理员与角色是一对多的关联;角色作为权限组的存在,又可以选择多个功能权限值与菜单,所以角色与菜单、功能权限值也是一对多的关系。

类图

Deparment与Position属于非核心,可以按照自己的实际业务进行扩展。

功能权限值初始化

  随着业务发展,需求功能是千奇百怪的,根本无法抽象出来,那么功能按钮就要随着业务进行定义。在我的项目里使用了枚举值进行定义每个功能权限,通过自定义的PermissionAttribute与响应的action进行绑定,在系统启动时,通过反射把功能权限的枚举值与相应的controller、action映射到MenuAction表,枚举值对应code字段,controller与action拼接后对应url字段。

  已初始化到数据库的权限值可以到菜单页把相对应的菜单与权限通过用户界面关联起来。

权限值绑定action

1         [HttpPost]
2         [Permission(PermCode.Administrator_Edit)]
3         public IActionResult Edit(EditModel edit)
4         {
5             //do something
6 
7             return Json(result);
8         }

初始化权限值

 1     /// <summary>2     /// 功能权限3     /// </summary>4     public static class PermissionUtil5     {6         public static readonly Dictionary<string, IEnumerable<int>> PermissionUrls = new Dictionary<string, IEnumerable<int>>();7         private static MongoRepository _mongoRepository;8 9         /// <summary>
10         /// 判断权限值是否被重复使用
11         /// </summary>
12         public static void ValidPermissions()
13         {
14             var codes = Enum.GetValues(typeof(PermCode)).Cast<int>();
15             var dic = new Dictionary<int, int>();
16             foreach (var code in codes)
17             {
18                 if (!dic.ContainsKey(code))
19                     dic.Add(code, 1);
20                 else
21                     throw new Exception($"权限值 {code} 被重复使用,请检查 PermCode 的定义");
22             }
23         }
24 
25         /// <summary>
26         /// 初始化添加预定义权限值
27         /// </summary>
28         /// <param name="app"></param>
29         public static void InitPermission(IApplicationBuilder app)
30         {
31             //验证权限值是否重复
32             ValidPermissions();
33 
34             //反射被标记的Controller和Action
35             _mongoRepository = (MongoRepository)app.ApplicationServices.GetService(typeof(MongoRepository));
36 
37             var permList = new List<MenuAction>();
38             var actions = typeof(PermissionUtil).Assembly.GetTypes()
39                 .Where(t => typeof(Controller).IsAssignableFrom(t) && !t.IsAbstract)
40                 .SelectMany(t => t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly));
41 
42             //遍历集合整理信息
43             foreach (var action in actions)
44             {
45                 var permissionAttribute =
46                     action.GetCustomAttributes(typeof(PermissionAttribute), false).ToList();
47                 if (!permissionAttribute.Any())
48                     continue;
49 
50                 var codes = permissionAttribute.Select(a => ((PermissionAttribute)a).Code).ToArray();
51                 var controllerName = action?.ReflectedType?.Name.Replace("Controller", "").ToLower();
52                 var actionName = action.Name.ToLower();
53 
54                 foreach (var item in codes)
55                 {
56                     if (permList.Exists(c => c.Code == item))
57                     {
58                         var menuAction = permList.FirstOrDefault(a => a.Code == item);
59                         menuAction?.Url.Add($"{controllerName}/{actionName}".ToLower());
60                     }
61                     else
62                     {
63                         var perm = new MenuAction
64                         {
65                             Id = item.ToString().EncodeMd5String().ToObjectId(),
66                             CreateDateTime = DateTime.Now,
67                             Url = new List<string> { $"{controllerName}/{actionName}".ToLower() },
68                             Code = item,
69                             Name = ((PermCode)item).GetDisplayName() ?? ((PermCode)item).ToString()
70                         };
71                         permList.Add(perm);
72                     }
73                 }
74                 PermissionUrls.TryAdd($"{controllerName}/{actionName}".ToLower(), codes);
75             }
76 
77             //业务功能持久化
78             _mongoRepository.Delete<MenuAction>(a => true);
79             _mongoRepository.BatchAdd(permList);
80         }
81 
82         /// <summary>
83         /// 获取当前路径
84         /// </summary>
85         /// <param name="filterContext"></param>
86         /// <returns></returns>
87         public static string CurrentUrl(HttpContext filterContext)
88         {
89             var url = filterContext.Request.Path.ToString().ToLower().Trim('/');
90             return url;
91         }
92     }

关联菜单与功能权限

访问权限

  当所有权限关系关联上后,用户访问系统时,需要对其所有操作进行拦截与实时的权限判断,我们注册一个全局的GlobalAuthorizeAttribute,其主要拦截所有已经标识PermissionAttribute的action,查询该用户所关联所有角色的权限是否满足允许通过。

  我的实现有个细节,给判断用户IsSuper==true,也就是超级管理员,如果是超级管理员则绕过所有判断,可能有人会问为什么不在角色添加一个名叫超级管理员进行判断,因为名称是不可控的,在代码逻辑里并不知道用户起的所谓的超级管理员,就是我们需要绕过验证的超级管理员,假如他叫无敌管理员呢?

 1  /// <summary>2     /// 全局的访问权限控制3     /// </summary>4     public class GlobalAuthorizeAttribute : System.Attribute, IAuthorizationFilter5     {6         #region 初始化7         private string _currentUrl;8         private string _unauthorizedMessage;9         private readonly List<string> _noCheckPage = new List<string> { "home/index", "home/indexpage", "/" };
10 
11         private readonly AdministratorService _administratorService;
12         private readonly MenuService _menuService;
13 
14         public GlobalAuthorizeAttribute(AdministratorService administratorService, MenuService menuService)
15         {
16             _administratorService = administratorService;
17             _menuService = menuService;
18         }
19         #endregion
20 
21         public void OnAuthorization(AuthorizationFilterContext context)
22         {
23             context.ThrowIfNull();
24 
25             _currentUrl = PermissionUtil.CurrentUrl(context.HttpContext);
26 
27             //不需要验证登录的直接跳过
28             if (context.Filters.Count(a => a is AllowAnonymousFilter) > 0)
29                 return;
30 
31             var user = GetCurrentUser(context);
32             if (user == null)
33             {
34                 if (_noCheckPage.Contains(_currentUrl))
35                     return;
36 
37                 _unauthorizedMessage = "登录失效";
38 
39                 if (context.HttpContext.Request.IsAjax())
40                     NoUserResult(context);
41                 else
42                     LogoutResult(context);
43                 return;
44             }
45 
46             //超级管理员跳过
47             if (user.IsSuper)
48                 return;
49 
50             //账号状态判断
51             var administrator = _administratorService.GetById(user.UserId);
52             if (administrator != null && administrator.Status != EAdministratorStatus.Normal)
53             {
54                 if (_noCheckPage.Contains(_currentUrl))
55                     return;
56 
57                 _unauthorizedMessage = "亲~您的账号已被停用,如有需要请您联系系统管理员";
58 
59                 if (context.HttpContext.Request.IsAjax())
60                     AjaxResult(context);
61                 else
62                     AuthResult(context, 403, GoErrorPage(true));
63 
64                 return;
65             }
66 
67             if (_noCheckPage.Contains(_currentUrl))
68                 return;
69 
70             var userUrl = _administratorService.GetUserCanPassUrl(user.UserId);
71 
72             // 判断菜单访问权限与菜单访问权限
73             if (IsMenuPass(userUrl) && IsActionPass(userUrl))
74                 return;
75 
76             if (context.HttpContext.Request.IsAjax())
77                 AuthResult(context, 200, GetJsonResult());
78             else
79                 AuthResult(context, 403, GoErrorPage());
80         }
81     }

功能权限

  在权限验证通过后,返回view之前,还是利用了Filter进行一个实时的权限查询,主要把该用户所拥有功能权限值查询出来通过ViewData["PermCodes"]传到页面,然后通过razor进行按钮的渲染判断。

  然而我在项目中封装了大部分常用的LayUI控件,主要利用.Net Core的TagHelper进行了封装,TagHelper内部与ViewData["PermCodes"]进行判断是否输出HTML。

全局功能权限值查询

 1 /// <summary>2     /// 全局用户权限值查询3     /// </summary>4     public class GobalPermCodeAttribute : IActionFilter5     {6         private readonly AdministratorService _administratorService;7 8         public GobalPermCodeAttribute(AdministratorService administratorService)9         {
10             _administratorService = administratorService;
11         }
12 
13         private static AdministratorData GetCurrentUser(HttpContext context)
14         {
15             return context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.UserData)?.Value.FromJson<AdministratorData>();
16         }
17 
18 
19         public void OnActionExecuting(ActionExecutingContext context)
20         {
21             ((Controller)context.Controller).ViewData["PermCodes"] = new List<int>();
22 
23             if (context.HttpContext.Request.IsAjax())
24                 return;
25 
26             var user = GetCurrentUser(context.HttpContext);
27             if (user == null)
28                 return;
29 
30             if (user.IsSuper)
31                 return;
32 
33             ((Controller)context.Controller).ViewData["PermCodes"] = _administratorService.GetActionCode(user.UserId).ToList();
34         }
35 
36         public void OnActionExecuted(ActionExecutedContext context)
37         {
38         }
39     }

LayUI Buttom的TagHelper封装

 1   [HtmlTargetElement("LayuiButton")]2     public class LayuiButtonTag : TagHelper3     {4         #region 初始化5         private const string PermCodeAttributeName = "PermCode";6         private const string ClasstAttributeName = "class";7         private const string LayEventAttributeName = "lay-event";8         private const string LaySubmitAttributeName = "LaySubmit";9         private const string LayIdAttributeName = "id";
10         private const string StyleAttributeName = "style";
11 
12         [HtmlAttributeName(StyleAttributeName)]
13         public string Style { get; set; }
14 
15         [HtmlAttributeName(LayIdAttributeName)]
16         public string Id { get; set; }
17 
18         [HtmlAttributeName(LaySubmitAttributeName)]
19         public string LaySubmit { get; set; }
20 
21         [HtmlAttributeName(LayEventAttributeName)]
22         public string LayEvent { get; set; }
23 
24         [HtmlAttributeName(ClasstAttributeName)]
25         public string Class { get; set; }
26 
27         [HtmlAttributeName(PermCodeAttributeName)]
28         public int PermCode { get; set; }
29 
30         [HtmlAttributeNotBound]
31         [ViewContext]
32         public ViewContext ViewContext { get; set; }
33 
34         #endregion
35         public override async void Process(TagHelperContext context, TagHelperOutput output)
36         {
37             context.ThrowIfNull();
38             output.ThrowIfNull();
39 
40             var administrator = ViewContext.HttpContext.GetCurrentUser();
41             if (administrator == null)
42                 return;
43 
44             var childContent = await output.GetChildContentAsync();
45 
46             if (((List<int>)ViewContext.ViewData["PermCodes"]).Contains(PermCode) || administrator.IsSuper)
47             {
48                 foreach (var item in context.AllAttributes)
49                 {
50                     output.Attributes.Add(item.Name, item.Value);
51                 }
52 
53                 output.TagName = "a";
54                 output.TagMode = TagMode.StartTagAndEndTag;
55                 output.Content.SetHtmlContent(childContent.GetContent());
56             }
57             else
58             {
59                 output.TagName = "";
60                 output.TagMode = TagMode.StartTagAndEndTag;
61                 output.Content.SetHtmlContent("");
62             }
63         }
64     }

 

视图代码

结尾

  以上就是我本篇分享的内容,项目是以单体应用提供的,方案思路也适用于前后端分离。最后附上几个系统效果图

 

 

 


作  者: 陈珙
出  处:https://www.cnblogs.com/skychen1218/p/13053878.html
关于作者:专注于微软平台的项目开发。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

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

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

相关文章

[数据结构-严蔚敏版]P46栈的顺序存储表示

大家如果发现代码有错误&#xff0c;麻烦评论告知一下!!! 代码如下: #include <iostream> using namespace std;const int STACK_INIT_SIZE 100; const int STACKINCREMENT 10;typedef int ElemType;typedef struct {ElemType *base;ElemType *top;int stacksize; }S…

ProcessOn使用

文章目录1. 拥有一个账号;2.思维导图&#xff1a;快速添加主题&#xff1a;移动主题位置和排序用格式刷来复制主题样式善用右键菜单快捷键后续使用时再补充 ^ ^1. 拥有一个账号; 2.思维导图&#xff1a; 快速添加主题&#xff1a; 移动主题位置和排序 用格式刷来复制主题样式…

基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(六)

系列文章使用 abp cli 搭建项目给项目瘦身&#xff0c;让它跑起来完善与美化&#xff0c;Swagger登场数据访问和代码优先自定义仓储之增删改查统一规范API&#xff0c;包装返回模型再说Swagger&#xff0c;分组、描述、小绿锁接入GitHub&#xff0c;用JWT保护你的API异常处理和…

[数据结构-严蔚敏版]P48栈的链式表示

代码如下: #include <iostream> using namespace std;typedef int ElemType;typedef struct SNode {ElemType data;SNode *next; }SNode;typedef struct {SNode *top; }SqStack;bool initStack(SqStack &s) {s.top nullptr;return true; }bool destroyStack(SqStac…

还有多少人不会用K8s?.NET高级高薪岗,开始要求了!

毫无疑问&#xff0c;Kubernetes已经成为容器编排事实标准。除了已经拥抱Kubernetes的Google、BAT、京东、奇虎360等巨头大厂外&#xff0c;更多的企业也都在向Kubernetes迁移。容器技术大势所趋&#xff0c;是互联网企业目前急需的技术人才之一&#xff0c;已成为运维工程师、…

[数据结构-严蔚敏版]P61ADT Queue的表示与实现(单链队列-队列的链式存储结构)

代码如下: #include <iostream> using namespace std;typedef int ElemType;typedef struct QNode {ElemType data;QNode *next; }QNode ,*QueuePtr;typedef struct {QueuePtr front;QueuePtr rear; }LinkQueue;bool initQueue(LinkQueue &q) {q.front q.rear new…

基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(七)

系列文章使用 abp cli 搭建项目给项目瘦身&#xff0c;让它跑起来完善与美化&#xff0c;Swagger登场数据访问和代码优先自定义仓储之增删改查统一规范API&#xff0c;包装返回模型再说Swagger&#xff0c;分组、描述、小绿锁接入GitHub&#xff0c;用JWT保护你的API异常处理和…

[数据结构-严蔚敏版]P64循环队列-队列的顺序存储结构

代码如下: #include <iostream> using namespace std;const int MAXQSIZE 10;typedef int ElemType;typedef struct {ElemType *base;int front;int rear; }SqQueue;bool initQueue(SqQueue &q) {q.base new ElemType[MAXQSIZE];if (!q.base) return false;q.fron…

BeetleX.FastHttpApi之Vuejs扩展

非常喜欢用vuejs,但又不想花时间去搞nodejs和webpack之类的&#xff0c;所以才有了BeetleX.FastHttpApi.VueExtend这样一个组件&#xff1b;组件的主要功能就是可以实现在vs.net中编写*.vue并直接引用到服务中&#xff0c;这样对于我这个习惯在vs.net写服务应的带来极大的方便性…

C++的new、delete需要注意的一点:使用危险函数导致的越界CRT detected that the application wrote to memory after end of heap

new、delete需要注意的一个特性 正常情况new一个数组之后&#xff0c;用delete释放是没有问题的。但是当对new得到的堆区进行越界的写入操作&#xff08;读操作不会&#xff09;将会导致delete时出现段错误&#xff0c;无法进行删除。如下面的程序所示&#xff1a; 数组大小只…

数据结构与算法专题——第四题 字符串相似度

这篇我们看看 最长公共子序列 的另一个版本&#xff0c;求字符串相似度(编辑距离)&#xff0c;我也说过了&#xff0c;这是一个非常实用的算法&#xff0c;在DNA对比&#xff0c;网页聚类等方面都有用武之地。一&#xff1a;概念对于两个字符串 A 和 B&#xff0c;通过基本的增…

[数据结构-严蔚敏版]P71串的抽象数据类型的定义

代码如下: #include <iostream> #include <string> using namespace std;typedef struct {char *ch;int length; }String;bool initString(String &s) {s.ch nullptr;s.length 0;return true; }bool strAssign(String &s, const char *ch) {int len st…

三分钟学会.NET Core Jwt 策略授权认证

一.前言大家好我又回来了&#xff0c;前几天讲过一个关于Jwt的身份验证最简单的案例&#xff0c;但是功能还是不够强大&#xff0c;不适用于真正的项目&#xff0c;是的&#xff0c;在真正面对复杂而又苛刻的客户中&#xff0c;我们会不知所措&#xff0c;就现在需要将认证授权…

[数据结构-严蔚敏版]P65离散事件模拟(银行客户的离散事件驱动模拟程序)

写这个简单玩意&#xff0c;居然花费了我6小时&#xff0c;唉!!!&#xff0c;还是太菜了! 中间已经起了放弃的念头了&#xff0c;最后还是坚持下来了! 总结&#xff1a; (1)漏了p p->next (2)队列删除元素的时候&#xff0c;删除的是最后一个忘记特判。 (3)写的时候太急了…

[温故知新] 编程原则和模式

写了这么多年代码&#xff0c;依旧做不好一个项目做好一个项目是人力、产品、业务、技术、运营的结合&#xff0c;可能还叠加一点时机的因素&#xff0c;就我们码农而言&#xff0c;工作就是搬砖&#xff0c;实现产品&#xff0c; 给业务提供支撑。“给祖传代码加 BUG 修 BUG”…

IntelliJ IDEA中快捷键大全+出现的问题

参照 文章目录1.Ctrl2.Alt3.Shift4.Ctrl Alt5.Ctrl Shift6.Alt Shift7.Ctrl Shift Alt8.其他idea如何将设置包名展开当出现右键&#xff0c;没有创建包选项时idea没有maven图标安装mysql和idea配置idea查看代码的最近修改人及时间1.Ctrl 快捷键介绍Ctrl F在当前文件进行…

基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(八)

系列文章使用 abp cli 搭建项目给项目瘦身&#xff0c;让它跑起来完善与美化&#xff0c;Swagger登场数据访问和代码优先自定义仓储之增删改查统一规范API&#xff0c;包装返回模型再说Swagger&#xff0c;分组、描述、小绿锁接入GitHub&#xff0c;用JWT保护你的API异常处理和…

[数据结构-严蔚敏版]P95矩阵压缩-特殊矩阵的存储(对称矩阵,三角矩阵)

对称矩阵的存储&#xff1a; 代码如下: #include <iostream> using namespace std;int main() {int n;cin >> n;int *a;a new int[(n*(n 1)) / 2];for (int i 0; i < (n*(n 1)) / 2; i){cin >> a[i];}for (int i 1; i < n; i){for (int j 1; j…

微前端与项目实施方案研究

一、前言微前端(micro-frontends)是近几年在前端领域出现的一个新概念&#xff0c;主要内容是将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块&#xff0c;而在用户看来仍然是内聚的单个产品。微前端的理念源于微服务&#xff0c;是将庞大的整体拆成可控的小…

ASP.NET Core分布式项目实战(集成ASP.NETCore Identity)--学习笔记

任务24&#xff1a;集成ASP.NETCore Identity之前在 Index 页面写了一个 strong 标签&#xff0c;需要加个判断再显示&#xff0c;不然为空没有错误的时候也会显示if (!ViewContext.ModelState.IsValid) {<strong>Error""</strong><div asp-validatio…