前言
上个月,我写了两篇微服务的文章:《.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 PermissionUtil 5 { 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 }