手把手撸套框架-权限系统设计
时间又过了一个月,终于熬过了试用期。 之前每天抽时间写完了代码生成器,算是为自己打下了一个不错基础。终于熬过了第二个项目。
但是我经常也会陷入各种迷思,现在各种技术都在换代,经常让我自我怀疑,
后端:.net 从framwork 往 core 转,
前端:Jquery+Bootstarp 往 Vue+Element 转
数据操作也 从原来的 写Sql 往 ORM框架转,
对我来说,本身就有三四年的 编码空白期,经常会恐惧要不要使用各种新东西,但是用上去的话,公司又没人能指导,出了问题也没人能帮。
所以,对我来说总结一条,对于技术选型尽快可能遵守 “通用技术”
比如 Vue,无论java,php,.net 都是通用的, 所以我在框架上 基本上 不用任何 Razor模板,包括最近出的那个Blazor。
这种出了问题,比较百度上的内容也能多点。。。
好吧,废话不多说了,进入今天的主题,权限系统设计。
想想上次做权限,都是12年前,读书时候的事情了,出来工作以后就没碰过这一块,刚工作头两年项目中都没有这个模块,小公司就这样。
后几年有技术大牛搞定了,而且过去几年都依赖winner框架有独立的权限系统,所以压根没想过这一块。
这不,我一上来第一反应就是要做一个 独立的权限系统 结果根本行不通,这和我现在任职的公司是有关系,现在这个公司 是一个工厂型企业,
虽然开发的是内部系统,比如销售的售后管理系统,文档管理系统的, 乍一看可以做一套 集中管理的权限,幸好没这么干,公司虽然是一个工厂型企业,
但也是个集团公司,下属好几个子公司,每个公司都有销售,每个公司 都要这个售后系统,和 文档管理系统,根本 不是我之前那种互联网企业的 平台型项目。
说白了,就是要那种小型的独立的内容管理系统。。所以要的就是内嵌权限管理。
这对我来说更好,想想读书那时候 那一套权限设计,十多年了依然适用。五张表权限设计:
简单明了,再做一个 视图,将这些全部串联起来,配合 .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岁之后再出发吧··!!!