手把手撸套框架-权限系统设计
时间又过了一个月,终于熬过了试用期。 之前每天抽时间写完了代码生成器,算是为自己打下了一个不错基础。终于熬过了第二个项目。
但是我经常也会陷入各种迷思,现在各种技术都在换代,经常让我自我怀疑,
后端:.net 从framwork 往 core 转,
前端:Jquery+Bootstarp 往 Vue+Element 转
数据操作也 从原来的 写Sql 往 ORM框架转,
对我来说,本身就有三四年的 编码空白期,经常会恐惧要不要使用各种新东西,但是用上去的话,公司又没人能指导,出了问题也没人能帮。
所以,对我来说总结一条,对于技术选型尽快可能遵守 “通用技术”
比如 Vue,无论java,php,.net 都是通用的, 所以我在框架上 基本上 不用任何 Razor模板,包括最近出的那个Blazor。
这种出了问题,比较百度上的内容也能多点。。。
好吧,废话不多说了,进入今天的主题,权限系统设计。
想想上次做权限,都是12年前,读书时候的事情了,出来工作以后就没碰过这一块,刚工作头两年项目中都没有这个模块,小公司就这样。
后几年有技术大牛搞定了,而且过去几年都依赖winner框架有独立的权限系统,所以压根没想过这一块。
这不,我一上来第一反应就是要做一个 独立的权限系统 结果根本行不通,这和我现在任职的公司是有关系,现在这个公司 是一个工厂型企业,
虽然开发的是内部系统,比如销售的售后管理系统,文档管理系统的, 乍一看可以做一套 集中管理的权限,幸好没这么干,公司虽然是一个工厂型企业,
但也是个集团公司,下属好几个子公司,每个公司都有销售,每个公司 都要这个售后系统,和 文档管理系统,根本 不是我之前那种互联网企业的 平台型项目。
说白了,就是要那种小型的独立的内容管理系统。。所以要的就是内嵌权限管理。
这对我来说更好,想想读书那时候 那一套权限设计,十多年了依然适用。五张表权限设计:
简单明了,再做一个 视图,将这些全部串联起来,配合 .net 过滤器,起来去还是比较舒服的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | 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岁之后再出发吧··!!!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南