.NET Core微服務 許可權系統+工作流(一)許可權系統
一、前言
實際上許可權系統老早之前我就在一直開發,大概在剛畢業沒多久就想一個人寫一個系統,斷斷續續一直堅持到現在,畢竟自己親動手自寫的系統才有收穫,本篇僅介紹許可權。
小小系統上不了檯面,望各位大神勿噴。
二、系統介紹
目前採用的是.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 下,後續會持續