.Net Core实战之基于角色的访问控制的设计
前言
上個(gè)月,我寫了兩篇微服務(wù)的文章:《.Net微服務(wù)實(shí)戰(zhàn)之技術(shù)架構(gòu)分層篇》與《.Net微服務(wù)實(shí)戰(zhàn)之技術(shù)選型篇》,微服務(wù)系列原有三篇,當(dāng)我憋第三篇的內(nèi)容時(shí)候一直沒有靈感,因此先打算放一放。
本篇文章與源碼原本打算實(shí)在去年的時(shí)候完成并發(fā)布的,然而我一直忙于公司項(xiàng)目的微服務(wù)的實(shí)施,所以該篇文章一拖再拖。如今我花了點(diǎn)時(shí)間整理了下代碼,并以此篇文章描述整個(gè)實(shí)現(xiàn)思路,并開放了源碼給予需要的人一些參考。
源碼:https://github.com/SkyChenSky/Sikiro.RBAC
RBAC
Role-Based Access Contro翻譯成中文就是基于角色的訪問控制,文章以下我都用他的簡(jiǎn)稱RBAC來描述。
現(xiàn)信息系統(tǒng)的權(quán)限控制大多數(shù)采取RBAC的思想進(jìn)行實(shí)現(xiàn),其本質(zhì)思想是對(duì)系統(tǒng)各種的操作權(quán)限不是直接授予具體的某個(gè)用戶,而是在用戶集合與權(quán)限集合之間建立一個(gè)角色,作為間接關(guān)聯(lián)。每一種角色對(duì)應(yīng)一組相應(yīng)的權(quán)限。一旦用戶被分配了適當(dāng)?shù)慕巧?#xff0c;該用戶就擁有此角色的所有操作權(quán)限。
通過以上的描述,我們可以分析出以下信息:
用戶與權(quán)限是通過角色間接關(guān)聯(lián)的
角色的本質(zhì)就是權(quán)限組(權(quán)限集合)
這樣做的好處在于,不必在每次創(chuàng)建用戶時(shí)都進(jìn)行分配權(quán)限的操作,只要分配用戶相應(yīng)的角色即可,而且角色的權(quán)限變更比用戶的權(quán)限變更要少得多,這樣將簡(jiǎn)化用戶的權(quán)限管理,減少系統(tǒng)的開銷。
功能分析
權(quán)限分類
從權(quán)限的作用可以分為三種,功能權(quán)限、訪問權(quán)限、數(shù)據(jù)權(quán)限:
功能權(quán)限
功能權(quán)限指系統(tǒng)用戶允許在頁面進(jìn)行按鈕操作的權(quán)限。如果有權(quán)限則功能按鈕展示,否則隱藏。
訪問權(quán)限
訪問權(quán)限指系統(tǒng)用戶通過點(diǎn)擊按鈕后進(jìn)行地址的請(qǐng)求訪問的權(quán)限(地址跳轉(zhuǎn)與接口請(qǐng)求),如果無權(quán)限訪問,則由頁面提示無權(quán)限訪問。
數(shù)據(jù)權(quán)限
數(shù)據(jù)權(quán)限指用戶可訪問系統(tǒng)的數(shù)據(jù)權(quán)限,不同的用戶可以訪問不同的數(shù)據(jù)粒度。
數(shù)據(jù)權(quán)限的實(shí)現(xiàn)可大可小,大可大到對(duì)條件進(jìn)行動(dòng)態(tài)配置,小可小到只針對(duì)某個(gè)維度進(jìn)行硬編碼。不納入這次的討論范圍。
用例圖
非功能性需求
時(shí)效性,直接影響到安全性,既然是權(quán)限控制,那么理應(yīng)一修改權(quán)限后就立刻生效。曾經(jīng)有同行問過我,是不是每一個(gè)請(qǐng)求都得去查一次數(shù)據(jù)庫(kù)是否滿足權(quán)限,如果是,數(shù)據(jù)庫(kù)壓力豈不是很大?
安全性,每一個(gè)頁面跳轉(zhuǎn),每一個(gè)讀寫請(qǐng)求都的進(jìn)行一次權(quán)限驗(yàn)證,不滿足的權(quán)限的功能按鈕就不需要渲染,避免樣式display:none的情況。
開發(fā)效率,權(quán)限控制理應(yīng)是框架層面的,因此盡可能作為非業(yè)務(wù)的侵入性,讓開發(fā)人員保持原有的數(shù)據(jù)善增改查與頁面渲染。
技術(shù)選型
LayUI
學(xué)習(xí)門檻極低,開箱即用。其外在極簡(jiǎn),卻又不失飽滿的內(nèi)在,體積輕盈,組件豐盈,從核心代碼到?API 的每一處細(xì)節(jié)都經(jīng)過精心雕琢,非常適合界面的快速開發(fā),它更多是為服務(wù)端程序員量身定做,無需涉足各種前端工具的復(fù)雜配置,只需面對(duì)瀏覽器本身,讓一切你所需要的元素與交互,從這里信手拈來。作為國(guó)人的開源項(xiàng)目,完整的接口文檔與Demo示例讓入門者非常友好的上手,開箱即用的Api讓學(xué)習(xí)成本盡可能的低,其易用性成為快速開發(fā)框架的基礎(chǔ)。
MongoDB
主要兩大優(yōu)勢(shì),無模式與橫向擴(kuò)展。對(duì)于權(quán)限模塊來說,無需SQL來寫復(fù)雜查詢和報(bào)表,也不需要使用到多表的強(qiáng)事務(wù),上面提到的時(shí)效性的數(shù)據(jù)庫(kù)壓力問題也可以通過分片解決。無模式使得開發(fā)人員無需預(yù)定義存儲(chǔ)結(jié)構(gòu),結(jié)合MongoDB官方提供的驅(qū)動(dòng)可以做到快速的開發(fā)。
數(shù)據(jù)庫(kù)設(shè)計(jì)
?E-R圖
?
一個(gè)管理員可以擁有多個(gè)角色,因此管理員與角色是一對(duì)多的關(guān)聯(lián);角色作為權(quán)限組的存在,又可以選擇多個(gè)功能權(quán)限值與菜單,所以角色與菜單、功能權(quán)限值也是一對(duì)多的關(guān)系。
類圖
Deparment與Position屬于非核心,可以按照自己的實(shí)際業(yè)務(wù)進(jìn)行擴(kuò)展。
功能權(quán)限值初始化
隨著業(yè)務(wù)發(fā)展,需求功能是千奇百怪的,根本無法抽象出來,那么功能按鈕就要隨著業(yè)務(wù)進(jìn)行定義。在我的項(xiàng)目里使用了枚舉值進(jìn)行定義每個(gè)功能權(quán)限,通過自定義的PermissionAttribute與響應(yīng)的action進(jìn)行綁定,在系統(tǒng)啟動(dòng)時(shí),通過反射把功能權(quán)限的枚舉值與相應(yīng)的controller、action映射到MenuAction表,枚舉值對(duì)應(yīng)code字段,controller與action拼接后對(duì)應(yīng)url字段。
已初始化到數(shù)據(jù)庫(kù)的權(quán)限值可以到菜單頁把相對(duì)應(yīng)的菜單與權(quán)限通過用戶界面關(guān)聯(lián)起來。
權(quán)限值綁定action
1 [HttpPost] 2 [Permission(PermCode.Administrator_Edit)] 3 public IActionResult Edit(EditModel edit) 4 { 5 //do something 6 7 return Json(result); 8 }初始化權(quán)限值
1 /// <summary>2 /// 功能權(quán)限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 /// 判斷權(quán)限值是否被重復(fù)使用 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($"權(quán)限值 {code} 被重復(fù)使用,請(qǐng)檢查 PermCode 的定義"); 22 } 23 } 24 25 /// <summary> 26 /// 初始化添加預(yù)定義權(quán)限值 27 /// </summary> 28 /// <param name="app"></param> 29 public static void InitPermission(IApplicationBuilder app) 30 { 31 //驗(yàn)證權(quán)限值是否重復(fù) 32 ValidPermissions(); 33 34 //反射被標(biāo)記的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 //業(yè)務(wù)功能持久化 78 _mongoRepository.Delete<MenuAction>(a => true); 79 _mongoRepository.BatchAdd(permList); 80 } 81 82 /// <summary> 83 /// 獲取當(dāng)前路徑 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 }關(guān)聯(lián)菜單與功能權(quán)限
訪問權(quán)限
當(dāng)所有權(quán)限關(guān)系關(guān)聯(lián)上后,用戶訪問系統(tǒng)時(shí),需要對(duì)其所有操作進(jìn)行攔截與實(shí)時(shí)的權(quán)限判斷,我們注冊(cè)一個(gè)全局的GlobalAuthorizeAttribute,其主要攔截所有已經(jīng)標(biāo)識(shí)PermissionAttribute的action,查詢?cè)撚脩羲P(guān)聯(lián)所有角色的權(quán)限是否滿足允許通過。
我的實(shí)現(xiàn)有個(gè)細(xì)節(jié),給判斷用戶IsSuper==true,也就是超級(jí)管理員,如果是超級(jí)管理員則繞過所有判斷,可能有人會(huì)問為什么不在角色添加一個(gè)名叫超級(jí)管理員進(jìn)行判斷,因?yàn)槊Q是不可控的,在代碼邏輯里并不知道用戶起的所謂的超級(jí)管理員,就是我們需要繞過驗(yàn)證的超級(jí)管理員,假如他叫無敵管理員呢?
1 /// <summary>2 /// 全局的訪問權(quán)限控制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 //不需要驗(yàn)證登錄的直接跳過 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 //超級(jí)管理員跳過 47 if (user.IsSuper) 48 return; 49 50 //賬號(hào)狀態(tài)判斷 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 = "親~您的賬號(hào)已被停用,如有需要請(qǐng)您聯(lián)系系統(tǒng)管理員"; 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 // 判斷菜單訪問權(quán)限與菜單訪問權(quán)限 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 }功能權(quán)限
在權(quán)限驗(yàn)證通過后,返回view之前,還是利用了Filter進(jìn)行一個(gè)實(shí)時(shí)的權(quán)限查詢,主要把該用戶所擁有功能權(quán)限值查詢出來通過ViewData["PermCodes"]傳到頁面,然后通過razor進(jìn)行按鈕的渲染判斷。
然而我在項(xiàng)目中封裝了大部分常用的LayUI控件,主要利用.Net Core的TagHelper進(jìn)行了封裝,TagHelper內(nèi)部與ViewData["PermCodes"]進(jìn)行判斷是否輸出HTML。
全局功能權(quán)限值查詢
1 /// <summary>2 /// 全局用戶權(quán)限值查詢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 }?
視圖代碼
結(jié)尾
以上就是我本篇分享的內(nèi)容,項(xiàng)目是以單體應(yīng)用提供的,方案思路也適用于前后端分離。最后附上幾個(gè)系統(tǒng)效果圖
?
?
?
作 者:?陳珙
出 處:https://www.cnblogs.com/skychen1218/p/13053878.html
關(guān)于作者:專注于微軟平臺(tái)的項(xiàng)目開發(fā)。如有問題或建議,請(qǐng)多多賜教!
版權(quán)聲明:本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
總結(jié)
以上是生活随笔為你收集整理的.Net Core实战之基于角色的访问控制的设计的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core加解密实战系列之——R
- 下一篇: 还有多少人不会用K8s?.NET高级高薪