手把手擼套框架-許可權系統設計
目錄
時間又過了一個月,終於熬過了試用期。 之前每天抽時間寫完了程式碼生成器,算是為自己打下了一個不錯基礎。終於熬過了第二個專案。
但是我經常也會陷入各種迷思,現在各種技術都在換代,經常讓我自我懷疑,
後端:.net 從framwork 往 core 轉,
前端:Jquery+Bootstarp 往 Vue+Element 轉
資料操作也 從原來的 寫Sql 往 ORM框架轉,
對我來說,本身就有三四年的 編碼空白期,經常會恐懼要不要使用各種新東西,但是用上去的話,公司又沒人能指導,出了問題也沒人能幫。
所以,對我來說總結一條,對於技術選型儘快可能遵守 “通用技術”
比如 Vue,無論java,php,.net 都是通用的, 所以我在框架上 基本上 不用任何 Razor模板,包括最近出的那個Blazor。
這種出了問題,比較百度上的內容也能多點。。。
好吧,廢話不多說了,進入今天的主題,許可權系統設計。
想想上次做許可權,都是12年前,讀書時候的事情了,出來工作以後就沒碰過這一塊,剛工作頭兩年專案中都沒有這個模組,小公司就這樣。
後幾年有技術大牛搞定了,而且過去幾年都依賴winner框架有獨立的許可權系統,所以壓根沒想過這一塊。
這不,我一上來第一反應就是要做一個 獨立的許可權系統 結果根本行不通,這和我現在任職的共i是有關係,現在這個公司 是一個工廠型企業,
雖然開發的是內部系統,比如銷售的售後管理系統,文件管理系統的, 乍一看可以做一套 集中管理的許可權,幸好沒這麼幹,公司雖然是一個工廠型企業,
但也是個集團公司,下屬好幾個子公司,每個公司都有銷售,每個共公司 都要這個售後系統,和 文件管理系統,根本 不是我之前那種網際網路企業的 平臺型專案。
說白了,就是要那種小型的獨立的內容管理系統。。所以要的就是內嵌許可權管理。
這對我來說更好,想想讀書那時候 那一套許可權設計,十多年了依然適用。五張表許可權設計:
簡單明瞭,再做一個 檢視,將這些全部串聯起來,配合 .net 過濾器,起來去還是比較舒服的。
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Victory.Template.DataAccess.CodeGenerator; using Victory.Template.Entity.CodeGenerator; using Victory.Template.Entity.Enums; using Victory.Template.Entity; namespace Victory.Template.WebApp.Attribute { public class RightAttribute : ActionFilterAttribute { /// <summary> /// 忽略許可權 /// </summary> public bool Ignore { get; set; } /// <summary> /// 許可權名稱 /// </summary> public string PowerName { get; set; } public override void OnActionExecuting(ActionExecutingContext Context) { base.OnActionExecuting(Context); //先取出登入使用者id int userid = int.Parse(Context.HttpContext.User.FindFirst("userId").Value); //根據配置檔案決定是否給初次登入的使用者 分配一個預設的登入角色 if (AppConfig.IsSetDefautlRole) { SetDefaultRole(userid); } //如果Ignore 為true 則表示不檢查該操作,這裡只給他初次登入分配 普通會員角色 if (Ignore) { return; } //獲取路由地址 string areaName = string.Empty; string controllerName = string.Empty; string actionName = string.Empty; string page = GetPageUrl(Context, ref areaName, ref controllerName, ref actionName); //判斷請求的 為訪問頁面 還是 請求功能操作 Ajax請求為功能, 非ajax請求為訪問頁面 var isAjax = Context.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest"; //判斷資料庫是否存在該許可權,不存則自動新增,無需手動配置 AddActionFunc(controllerName, actionName, areaName, page, isAjax); //如果全域性配置忽略許可權,則忽略檢測 if (AppConfig.IgnoreAuthRight) { return; } //若該使用者存在該頁面許可權,則直接return Tright_User_Role_Da userrole = new Tright_User_Role_Da(); if (userrole.ListByVm(userid, page).Count() > 0) { return; } //是否ajax請求,是ajax 則判定為 請求操作, 非ajax則判定為 訪問頁面 if (isAjax) { Context.Result = new JsonResult(new { Success = false, Code = 405, Message = "您沒有該功能操作許可權!" }); return; } //跳轉指定的沒有許可權的頁面 Context.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "UserRight", action = "NoPermission" })); return; } /// <summary> /// 給使用者設定預設登入角色 /// </summary> /// <returns></returns> public void SetDefaultRole(int userid) { Tright_User_Role_Da userrole = new Tright_User_Role_Da(); if (userrole.Where(s => s.Userid == userid).Count() <= 0) { Tright_User_Role userolemodel = new Tright_User_Role() { Roleid = 1, //預設1為普通會員 Userid = userid }; userrole.Insert(userolemodel); } } /// <summary> /// 獲取當前頁面 或 功能 的路由地址 /// </summary> /// <param name="Context"></param> /// <returns></returns> public string GetPageUrl(ActionExecutingContext Context, ref string areaName,ref string controllerName, ref string actionName) { if (Context.ActionDescriptor.RouteValues.ContainsKey("area")) { areaName = Context.ActionDescriptor.RouteValues["area"].ToString(); } if (Context.ActionDescriptor.RouteValues.ContainsKey("controller")) { controllerName = Context.ActionDescriptor.RouteValues["controller"].ToString(); } if (Context.ActionDescriptor.RouteValues.ContainsKey("action")) { actionName = Context.ActionDescriptor.RouteValues["action"].ToString(); } var page = "/" + controllerName + "/" + actionName; if (!string.IsNullOrEmpty(areaName)) { page = "/" + areaName + page; } return page; } /// <summary> /// 根據Action自動新增功能 /// </summary> /// <returns></returns> public void AddActionFunc(string controllerName,string actionName,string areaName,string page,bool isAjax) { //資料庫是否存在該頁面配置 Tright_Power_Da pwmanager = new Tright_Power_Da(); bool HasPage = pwmanager.Where(s => s.Pageurl.ToLower() == page.ToLower()).Count() <= 0; if (HasPage) { Tright_Power powermodel = new Tright_Power { Controller = controllerName, Action = actionName, Area = areaName, Powername = PowerName, Pageurl = page.ToLower() }; if (isAjax) { // 新增一個功能功能操作的許可權 var m = pwmanager.Where(s => s.Controller == controllerName && s.Powertype == (int)PowerType.頁面訪問).First(); powermodel.Parentid = m.Id; powermodel.Powertype = (int)PowerType.功能操作; } else { //新增一個 頁面訪問 許可權 powermodel.Parentid = 0; powermodel.Powertype = (int)PowerType.頁面訪問; } pwmanager.Insert(powermodel); } } } }
使用期起來也特別方便,打個特性類就型:
[Right(PowerName = "人員資訊")] public IActionResult Index() { return View(); }
上效果圖:
但是,我的第二個專案是個文件管理系統,有個要求,要求某些檔案某些人能看,某些人不能看,這套許可權就完全做不到了,而且像我們公司這樣的企業。
還涉及到有些檔案,某些部門的人能看,有些部門的不能看。 說 白了 就是 五張表的這種許可權設計, 有兩個問題:
1,許可權 不能控制檔案。
2,沒有使用者組。
別看我從事網際網路十年,以前用的許可權,還真沒有涉及這兩塊,只是知道有使用者組許可權,但是以前做的專案,完全都沒涉及到這一塊。
這不,到處百度,Github上下了幾個專案看了看, 感覺都挺扯淡的, 總之沒看到一個符合我上面那兩個需求的,多數一想,應該是我百度的方式不對。。。
有一天中午跟同事無意聊起這個話題,同事跟我說了一個詞語 “RBAC” 和 “ACL” 瞬間表示 不懂, 回來百度有一下。。。呀···! 原來我前面那種五張表設計
也屬於 RBAC,原來還專門有個這名詞,和一套理論體系。。。 翻了翻,芭拉 巴拉。。。反正沒看特別懂,主要是現在心態越來越浮躁了,真的有那種三十歲以後學習能力跟不上的感覺。
雖然沒看特別懂,但是知道。。這就是我想要的。不管了。直接上手畫表圖吧。
參考資料:https://www.cnblogs.com/jpfss/p/11210694.html
這裡,由於業務各有不同,所以 我這裡有些表精簡了欄位,值得一提的是,我也有看到 有些表設計 使用者組 表 Tright_Group 那裡 並沒設計Parent_ID ,也就是說使用者組 (部門)沒有層級關係。
有的 角色表 Tright_Role 有Parent_ID, 大概意思是 角色 可以繼承。 無論是 角色可以繼承 還是 使用者組 可以繼承 都是標識,許可權可以繼承。 這個我沒有去深究,反正。我現在任職的這個功能
很麻煩, 五級部門。所以 使用者組 那裡 是一定要設計 Parent_ID 的。
這裡資料庫我用的 Sqlserver,(其實,我更熟悉oracle) 這裡貼一下建表的sql:
CREATE TABLE [Tright_File] ( [Id] int NOT NULL, [File_Name] varchar(255) NULL, [File_Url] varchar(255) NULL, [Status] int NULL, CONSTRAINT [tright_file_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO EXEC sp_addextendedproperty 'MS_Description', N'檔案表' GO CREATE TABLE [Tright_Group] ( [Id] int NOT NULL, [Group_Name] varchar(255) NULL, [Parent_Id] int NULL, [Status] int NULL, CONSTRAINT [pk_tright_group_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO EXEC sp_addextendedproperty 'MS_Description', N'使用者組' GO CREATE TABLE [Tright_Group_Role] ( [Id] int NOT NULL, [Group_Id] int NULL, [Role_Id] int NULL, CONSTRAINT [_copy_2_copy_2] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO EXEC sp_addextendedproperty 'MS_Description', N'使用者組_角色中間表' GO CREATE TABLE [Tright_Menu] ( [Id] int NOT NULL, [Menu_Name] varchar(255) NULL, [Menu_Url] varchar(255) NULL, [Parent_Id] int NULL, [Status] int NULL, CONSTRAINT [tright_menu_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO CREATE TABLE [Tright_Operation] ( [Id] int NOT NULL, [Code] varchar(255) NULL, [Area] varchar(255) NULL, [Controller] varchar(255) NULL, [Action] varchar(255) NULL, [Url] varchar(255) NULL, [SortId] int NULL, [Status] int NULL, CONSTRAINT [tright_operation_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO CREATE TABLE [Tright_PageElement] ( [Id] int NOT NULL, [Element_Name] varchar(255) NULL, [Status] int NULL, CONSTRAINT [tright_pageelement_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO EXEC sp_addextendedproperty 'MS_Description', N'狀態' GO CREATE TABLE [Tright_Power] ( [Id] int NOT NULL, [Power_Type] varchar(255) NULL, CONSTRAINT [tright_power_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO CREATE TABLE [Tright_Power_Element] ( [Id] int NOT NULL, [Page_Id] int NULL, [Power_Id] int NULL, CONSTRAINT [tright_power_element_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO CREATE TABLE [Tright_Power_File] ( [Id] int NOT NULL, [File_Id] int NULL, [Power_Id] int NULL, CONSTRAINT [tright_power_file_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO CREATE TABLE [Tright_Power_Menu] ( [Id] int NOT NULL, [Menu_Id] int NULL, [Power_Id] int NULL, CONSTRAINT [tright_power_menu_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO CREATE TABLE [Tright_Power_Opeartion] ( [Id] int NOT NULL, [Operation_Id] int NULL, [Power_Id] int NULL, CONSTRAINT [tright_power_opeartion_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO CREATE TABLE [Tright_Role] ( [Id] int NOT NULL, [RoleName] varchar(255) NULL, [Status] int NULL, CONSTRAINT [pk_tright_role_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO CREATE TABLE [Tright_Role_Power] ( [Id] int NOT NULL, [Role_Id] int NULL, [Power_Id] int NULL, CONSTRAINT [tright_role_power_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO CREATE TABLE [Tright_User_Group] ( [Id] int NOT NULL, [User_Id] int NULL, [Group_Id] int NULL, CONSTRAINT [pk_tright_user_group_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO EXEC sp_addextendedproperty 'MS_Description', N'使用者_使用者組中間表' GO CREATE TABLE [Tright_User_Role] ( [Id] int NOT NULL, [User_Id] varchar(255) NULL, [Role_Id] NULL, CONSTRAINT [pk_tright_user_id] PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO EXEC sp_addextendedproperty 'MS_Description', N'使用者id' GO EXEC sp_addextendedproperty 'MS_Description', N'角色id' GO EXEC sp_addextendedproperty 'MS_Description', N'使用者_角色中間表' GO CREATE TABLE [Tsys_User] ( [Id] int NOT NULL, [User_Name] varchar(255) NULL, [User_Pwd] varchar(255) NULL, PRIMARY KEY CLUSTERED ([Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) GO EXEC sp_addextendedproperty 'MS_Description', N'使用者表' GO ALTER TABLE [Tright_Group_Role] ADD CONSTRAINT [fk_Tright_Group_Role_Groupid] FOREIGN KEY ([Group_Id]) REFERENCES [Tright_Group] ([Id]) GO ALTER TABLE [Tright_Group_Role] ADD CONSTRAINT [fk_Tright_Group_Role_Roleid] FOREIGN KEY ([Role_Id]) REFERENCES [Tright_Role] ([Id]) GO ALTER TABLE [Tright_Power_Element] ADD CONSTRAINT [fk_Tright_Power_Element_PageId] FOREIGN KEY ([Page_Id]) REFERENCES [Tright_PageElement] ([Id]) GO ALTER TABLE [Tright_Power_Element] ADD CONSTRAINT [fk_Tright_Power_Element_PowerId] FOREIGN KEY ([Power_Id]) REFERENCES [Tright_Power] ([Id]) GO ALTER TABLE [Tright_Power_File] ADD CONSTRAINT [fk_Tright_Power_File_FileId] FOREIGN KEY ([File_Id]) REFERENCES [Tright_File] ([Id]) GO ALTER TABLE [Tright_Power_File] ADD CONSTRAINT [fk_Tright_Power_File_PowerId] FOREIGN KEY ([Power_Id]) REFERENCES [Tright_Power] ([Id]) GO ALTER TABLE [Tright_Power_Menu] ADD CONSTRAINT [fk_Tright_Power_Menu_MenuId] FOREIGN KEY ([Menu_Id]) REFERENCES [Tright_Menu] ([Id]) GO ALTER TABLE [Tright_Power_Menu] ADD CONSTRAINT [fk_Tright_Power_Menu_PowerId] FOREIGN KEY ([Power_Id]) REFERENCES [Tright_Power] ([Id]) GO ALTER TABLE [Tright_Power_Opeartion] ADD CONSTRAINT [fk_Tright_Power_Opeartion_OpeartionId] FOREIGN KEY ([Operation_Id]) REFERENCES [Tright_Operation] ([Id]) GO ALTER TABLE [Tright_Power_Opeartion] ADD CONSTRAINT [fk_Tright_Power_Opeartion_PowerId] FOREIGN KEY ([Power_Id]) REFERENCES [Tright_Power] ([Id]) GO ALTER TABLE [Tright_Role_Power] ADD CONSTRAINT [fk_Tright_Role_Powe_Powerid] FOREIGN KEY ([Power_Id]) REFERENCES [Tright_Power] ([Id]) GO ALTER TABLE [Tright_Role_Power] ADD CONSTRAINT [fk_Tright_Role_Powe_Roleid] FOREIGN KEY ([Role_Id]) REFERENCES [Tright_Role] ([Id]) GO ALTER TABLE [Tright_User_Group] ADD CONSTRAINT [fk_Tright_User_Group_Userid] FOREIGN KEY ([User_Id]) REFERENCES [Tsys_User] ([Id]) GO ALTER TABLE [Tright_User_Group] ADD CONSTRAINT [fk_Tright_User_Group_Groupid] FOREIGN KEY ([Group_Id]) REFERENCES [Tright_Group] ([Id]) GO ALTER TABLE [Tright_User_Role] ADD CONSTRAINT [fk_Tright_User_Role_Roleid] FOREIGN KEY ([Role_Id]) REFERENCES [Tright_Role] ([Id]) GO ALTER TABLE [Tright_User_Role] ADD CONSTRAINT [fk_Tright_User_Role_Userid] FOREIGN KEY ([User_Id]) REFERENCES [Tsys_User] ([Id]) GO
SQL 是由 設計工具生成的,所以外來鍵命名 有點亂。我也沒心思去改了,我是直接刪掉了,現在建資料庫,我基本都不建外來鍵了。。。。
其實一套小型框架,主要就是 這麼幾件事,登入,許可權管理,系統日誌,。剩下的都可以用開源的工具去組裝,比如ORM用FreeSql,用log4net 去寫日誌,NPOI做匯入匯出。 前端要不Element UI 要不就 Bootstarp框架。
關鍵是 把技術定型。 不去東試試,西試試。 定型下來之後 就可以專心關注 核心業務。 另外,抽出來的框架部分,也可以持續更新去做 有 積累的開發。。
先寫到這裡 ,其實前端,分層框架 也做完了,但是隨著這次許可權升級,也會做一次更新。下次放出來,具體自己說的6個擼套框架,其實最近轉正之後 ,整個人鬆懈很多。。還是得繼續,畢竟自己的人生規劃就是未來三年
就在這種企業,先把創業失敗欠的錢先還清。。。 35歲之後再出發吧··!!!