.Net Core實戰之基於角色的訪問控制的設計,.Net微服務實戰之技術架構分層篇,.Net微服務實戰之技術選型篇

前言

  上個月,我寫了兩篇微服務的文章:《.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     }

關聯菜單與功能權限

訪問權限

  當所有權限關係關聯上后,用戶訪問系統時,需要對其所有操作進行攔截與實時的權限判斷,我們註冊一個全局的GlobalAuthorizeAttribute,其主要攔截所有已經標識PermissionAttribute的action,查詢該用戶所關聯所有角色的權限是否滿足允許通過。

  我的實現有個細節,給判斷用戶IsSuper==true,也就是超級管理員,如果是超級管理員則繞過所有判斷,可能有人會問為什麼不在角色添加一個名叫超級管理員進行判斷,因為名稱是不可控的,在代碼邏輯里並不知道用戶起的所謂的超級管理員,就是我們需要繞過驗證的超級管理員,假如他叫無敵管理員呢?

 1  /// <summary>
 2     /// 全局的訪問權限控制
 3     /// </summary>
 4     public class GlobalAuthorizeAttribute : System.Attribute, IAuthorizationFilter
 5     {
 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 : IActionFilter
 5     {
 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 : TagHelper
 3     {
 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     }

 

視圖代碼

結尾

  以上就是我本篇分享的內容,項目是以單體應用提供的,方案思路也適用於前後端分離。最後附上幾個系統效果圖

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!