一、前言
实际上权限系统老早之前我就在一直开发,大概在刚毕业没多久就想一个人写一个系统,断断续续一直坚持到现在,毕竟自己亲动手自写的系统才有收获,本篇仅介绍权限。
小小系统上不了台面,望各位大神勿喷。
二、系统介绍
目前采用的是.Net Core微服务的方式实现,本文不讨论具体的中间件主要是(ocelot + consul等),一直参考微软的 eShopOnContainers ,进行简单的实现,但是ORM是用的Dapper,并简单进行封装 传送门 ,当然自己也封装了一些简单的插件进行复用:传送门,如下:
三、权限系统
权限系统实现很简单,权限的划分我觉得可以分为三种:
1、菜单权限2、按钮权限3、数据权限
简单介绍下:1、菜单权限。表示用户是否能够访问该页面(角色挂钩)
2、按钮权限。表示用户是否能够操作该页面上的功能(角色挂钩)
3、数据权限。表示用户访问页面时进行数据筛选(该功能暂未实现,这个要与具体的业务结合才能写),与部门挂钩,这个不太好理解,当然一般的权限系统这个功能也不会做,举个简单例子,OA系统里面我查看我的工资条,我应该只能看到我自己的数据,但是我的部门经理,他可以有权限看到该部门的全部数据,这个就是数据权限。
为什么写这个系统?
之前待过好几家公司,发现他们的系统都是对菜单进行分配,当然了,业务需求只要这个就当我没说,我只是觉得这样做太不安全并且我觉得之前系统的实现方式可以进行一些优化,所以就一直写到现在,可能代码质量不如哪些大神的优秀,系统在我看来太小,就简单搭了个框架实现。你过条小水沟,没必要造条桥。
要使用该系统前提条件:前端:Sea.js和Vue,对于sea.js,在前端这块感觉已经没多少人用了,但是这中CMD思想是不会被淘汰的,你看最近比较火的layerui也是的,对于Vues只是简单的应用,也就用到双向绑定而已,开发复杂的页面确实比较方便,但是简单的页面就得不偿失了。
后端:consul、rabbitmq ,具体怎么安装不在描述
大概的用户访问流程描述如下:
用户登录 =====》 获取该用户角色 ===》 通过角色获取该角色对应的权限 并集 ===>返回相应数据
sys_user_role sys_role_resource
系统关系图如下(MySQL):
具体功能实现请看代码,这里不做阐述,菜单权限的分配通过角色表和菜单表的关联表操作即可,但是按钮的权限分配如何实现?我的实现方式是:把按钮的操作也看成一种菜单的资源分配,只不过比较特殊,我这里不仅仅是对按钮的显示进行控制,我做的比较绝,也对后台方法访问权限也做了控制,这样比较安全,对于按钮权限的控制,实际上是明确的,比方说,一个删除按钮,它只能对应后台的一个删除方法,这个方法是明确的,对于页面的按钮的类型和个数是固定的,不然你没办法分配,基于这个前提,我对菜单的生成进行代码控制从而达到控制目的,因此,菜单和按钮和在一起称之为资源表 sys_resource 。具体的实现代码也不是很复杂,一层一层判断即可,权限过滤器如下:
1 public class PermissionAuthorizationRequirement : IAuthorizationRequirement 2 { 3 public UrlAndButtonType UrlAndButtonType { get; } 4 5 public PermissionAuthorizationRequirement(string url, ButtonType buttonType, bool isPage) 6 { 7 UrlAndButtonType = new UrlAndButtonType() 8 { 9 Url = url, 10 ButtonType = (byte)buttonType, 11 IsPage = isPage 12 }; 13 } 14 public PermissionAuthorizationRequirement(string url, byte buttonType, bool isPage) 15 { 16 UrlAndButtonType = new UrlAndButtonType() 17 { 18 Url = url, 19 ButtonType = buttonType, 20 IsPage = isPage 21 }; 22 } 23 } 24 /// <summary> 25 /// 权限过滤器 26 /// </summary> 27 [Authorize] 28 [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 29 public sealed class PermissionAttribute : TypeFilterAttribute 30 { 31 /// <summary> 32 /// 构造器 33 /// </summary> 34 /// <param name="url">地址</param> 35 /// <param name="buttonType">按钮类型</param> 36 /// <param name="isPage">是否是页面</param> 37 public PermissionAttribute(string url = default(string), ButtonType buttonType = ButtonType.View, bool isPage = true) : 38 base(typeof(RequiresPermissionAttributeExecutor)) 39 { 40 Arguments = new object[] { new PermissionAuthorizationRequirement(url, buttonType, isPage) }; 41 } 42 /// <summary> 43 /// 构造器 44 /// </summary> 45 /// <param name="url">地址</param> 46 /// <param name="buttonType">按钮类型</param> 47 /// <param name="isPage">是否是页面</param> 48 public PermissionAttribute(string url, byte buttonType, bool isPage = true) : 49 base(typeof(RequiresPermissionAttributeExecutor)) 50 { 51 Arguments = new object[] { new PermissionAuthorizationRequirement(url, buttonType, isPage) }; 52 } 53 54 private class RequiresPermissionAttributeExecutor : Attribute, IAsyncResourceFilter 55 { 56 private IPermissionStorageContainer _permissionStorage; 57 private PermissionAuthorizationRequirement _requiredPermissions; 58 59 public RequiresPermissionAttributeExecutor( 60 IPermissionStorageContainer permissionStorage, PermissionAuthorizationRequirement requiredPermissions) 61 { 62 _permissionStorage = permissionStorage; 63 _requiredPermissions = requiredPermissions; 64 } 65 66 public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) 67 { 68 string menuUrl = _requiredPermissions.UrlAndButtonType.Url; 69 //判断用户权限 70 if (string.IsNullOrEmpty(menuUrl)) 71 { 72 //区域判断 73 string area = context.RouteData.Values["area"].ToString(); 74 if (string.IsNullOrEmpty(area)) 75 { 76 menuUrl = "/" + context.RouteData.Values["controller"] + "/" + context.RouteData.Values["action"]; 77 } 78 else 79 { 80 menuUrl = "/" + area + "/" + context.RouteData.Values["controller"] + "/" + context.RouteData.Values["action"]; 81 } 82 } 83 menuUrl = menuUrl.Trim().ToLower(); 84 var dbpermission = await _permissionStorage.GetPermissionAsync(); 85 var menu = dbpermission.Menus.FirstOrDefault(m => m.MenuUrl != null && m.MenuUrl.Trim().ToLower() == menuUrl); 86 if (menu != null)//地址存在 87 { 88 if (_requiredPermissions.UrlAndButtonType.ButtonType == default(byte)) 89 { 90 await next(); 91 } 92 else 93 { 94 byte buttonType = (byte)_requiredPermissions.UrlAndButtonType.ButtonType; 95 if (menu.MenuButton.Select(m => m.ButtonType).Contains(buttonType))//拥有操作权限 96 { 97 await next(); 98 } 99 else 100 { 101 //没有操作权限 102 if (_requiredPermissions.UrlAndButtonType.IsPage) 103 { 104 context.Result = new RedirectResult("/error/noauth"); 105 } 106 else 107 { 108 context.Result = new ContentResult() 109 { 110 Content = PermissionStatusCodes.Status2Unauthorized.ToString() 111 }; 112 } 113 await context.Result.ExecuteResultAsync(context); 114 } 115 } 116 } 117 else 118 { 119 //没有操作权限 120 if (_requiredPermissions.UrlAndButtonType.IsPage) 121 { 122 context.Result = new RedirectResult("/error/noauth"); 123 } 124 else 125 { 126 context.Result = new ContentResult() 127 { 128 Content = PermissionStatusCodes.Status2Unauthorized.ToString() 129 }; 130 } 131 await context.Result.ExecuteResultAsync(context); 132 } 133 } 134 } 135 136 }
在对于的页面添加过滤器即可,如下:
1 [HttpGet] 2 [Permission] 3 public async Task<IActionResult> Index(int pageIndex=1,int pageSize=10) 4 { 5 var res = await _messageService.GetPageAsync(pageIndex, pageSize); 6 return View(res); 7 } 8 [HttpGet] 9 [Permission("/Sys/Message/Index", ButtonType.View)] 10 public IActionResult Show() 11 { 12 return View(); 13 }
系统界面展示图:后台模板是之前从网上找的并自己简单改了一下,将就能看吧,实在不想花功夫在前端上面了@-^-@
运行步骤:1、确保数据库mssystem和mssystemlog存在 github文档中
2、consul服务启动,如下回车运行
3、VS项目启动
管理员登录账号wms,密码:所有账号密码都是123
代码地址:
https://github.com/wangmaosheng/MsSystem-BPM-ServiceAndWebApps
如果觉得有点作用的话,可以 start 下,后续会持续更新