拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ASP.Net RBAC权限管理及快速开发框架设计

简介这是一份面向中小型项目团队的 ASP.NET 权限管理与快速开发框架资源包集成组织机构、角色用户、权限授权、多系统多应用管理、定时任务、业务单据编码规则及代码生成器等核心模块适合需要快速搭建后台管理系统的 .NET 开发者。框架基于 Asp.Net Core MVC、EF、Dapper、WebAPI、Swagger 与 Vue 构建兼顾后端分层与前端交互架构清晰且易于扩展。压缩包共 367 个文件大小约 4.59MB其中 44 个 cs 源码文件与 30 个 cshtml 视图文件构成主体框架38 个 js、17 个 css 及 scss/less 样式文件负责前端展示另有项目工程、配置文件、图片及文档等目录结构清晰便于按模块检索学习。资源发布以来已有 81 人学习浏览通过该框架可掌握权限控制的完整实现思路借助内置代码生成器加速业务开发适合作为中小项目的基础脚手架或 .NET 技术学习参考。1. 用现成的“权限快速开发框架”而不是从零搭一套动业务代码之前权限模块往往是第一个被赶着交付、又最容易返工的部分。用户的登录状态、菜单的显示范围、按钮的可见性每个系统都跑不掉可一旦用户、角色、资源和权限的关系没放对位置等业务表铺开再回头改牵连面积非常大。这套基于 ASP.Net 的权限管理及快速开发框架就把前置问题打包解决RBAC 数据模型、登录过滤器、统一响应、通用 CRUD 先固化成工程骨架解开 zip 后先跑通授权链路再写业务省掉大量低水平复制粘贴。适合用 .NET 技术栈做企业应用、管理后台和中小型内部系统的开发者想自建团队脚手架的人也可以直接参考这套拆分思路。2. 权限管理的数据模型RBAC 表设计2.1 为什么不用“用户表里直接放权限码”早期项目里最容易出现的一种做法是在 Sys_User 表上加一个 Permissions 字段用逗号拼一长串权限码比如 user:add,user:edit。写前两个页面非常快但项目往后走三个迹象都会逼着人回头重构。第一个迹象是想统计“谁有导出权限”只能用 LIKE 去匹配一个逗号分隔的长字符串第二个迹象是某个角色的权限要整体调换只能写多条 UPDATE 去改所有相关人员的记录第三个迹象是新人入职管理员得对着权限码手工勾选权限完全无法按岗位打包。这就是为什么权限管理框架几乎清一色选择 RBAC即基于角色的访问控制。它的核心转移是用户不直接持有权限用户只挂角色角色才关联权限资源。岗位调整就换角色用户表不用批量更新权限变更的范围被牢牢限制在角色一层。2.2 核心表结构用户表、角色表、资源表与两张关联表RBAC 落到关系型数据库一般拆成 5 张表用户表 Sys_User、角色表 Sys_Role、用户角色关联表 Sys_UserRole、资源权限表 Sys_Permission、角色资源关联表 Sys_RolePermission。资源表把菜单和按钮放在一起用 ResourceType 区分避免按钮权限另起一套表结构和一套查询路径。表名关键字段职责Sys_UserId、UserName、PasswordHash、Status用户登录信息不直接放权限码Sys_RoleId、RoleCode、RoleName、IsSystem角色定义RoleCode 用于代码里做固定判断Sys_UserRoleUserId、RoleId 复合主键用户与角色的多对多关联Sys_PermissionId、ParentId、Name、PermissionCode、ResourceType、Url、Sort菜单和按钮的统一资源表Sys_RolePermissionRoleId、PermissionId 复合主键角色与资源的绑定关系这里有几个容易忽略的设计点。ParentId 和 Sort 负责菜单树的父子关系与展示顺序层级尽量控制在四层以内否则权限维护界面会非常难用。PermissionCode 做全表唯一约束代码里的权限特性、前端按钮的判断、数据库里的记录三处必须完全一致。ResourceType 建议用数值枚举比如 1 表示菜单、2 表示按钮不要直接存中文字符串否则查询条件和接口文档都要跟着写很多别名。2.3 建表语句与登录后的权限码联查下面这份建表语句覆盖上述 5 张表SQL Server 方向用原生 SQL 或 Dapper 可以直接跑-- 用户表 CREATE TABLE Sys_User ( Id INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(64) NOT NULL UNIQUE, PasswordHash NVARCHAR(128) NOT NULL, DisplayName NVARCHAR(64) NOT NULL, Status TINYINT NOT NULL DEFAULT 1, -- 1 正常0 禁用 LastLoginTime DATETIME NULL ); -- 角色表 CREATE TABLE Sys_Role ( Id INT IDENTITY(1,1) PRIMARY KEY, RoleCode NVARCHAR(64) NOT NULL UNIQUE, RoleName NVARCHAR(64) NOT NULL, IsSystem BIT NOT NULL DEFAULT 0 -- 系统角色不允许删除 ); -- 用户角色关联表 CREATE TABLE Sys_UserRole ( UserId INT NOT NULL REFERENCES Sys_User(Id), RoleId INT NOT NULL REFERENCES Sys_Role(Id), PRIMARY KEY (UserId, RoleId) ); -- 资源权限表菜单和按钮共用一张表 CREATE TABLE Sys_Permission ( Id INT IDENTITY(1,1) PRIMARY KEY, ParentId INT NULL REFERENCES Sys_Permission(Id), Name NVARCHAR(64) NOT NULL, PermissionCode NVARCHAR(128) NOT NULL UNIQUE, ResourceType TINYINT NOT NULL DEFAULT 1, -- 1 菜单2 按钮 Url NVARCHAR(256) NULL, Sort INT NOT NULL DEFAULT 0 ); -- 角色资源关联表 CREATE TABLE Sys_RolePermission ( RoleId INT NOT NULL REFERENCES Sys_Role(Id), PermissionId INT NOT NULL REFERENCES Sys_Permission(Id), PRIMARY KEY (RoleId, PermissionId) );关于两张关联表的复合主键需要说明Sys_UserRole 和 Sys_RolePermission 都不设自增 Id直接以 UserIdRoleId、RoleIdPermissionId 做主键天然阻止重复绑定也少建了两个无意义的索引。PasswordHash 列留到 128 字节是因为老 .NET 项目切换密码哈希算法时 SHA1、SHA256、PBKDF2 输出长度不一样留宽一点就不用动表结构。登录成功之后框架在这里做一次权限码汇总查询SELECT DISTINCT p.PermissionCode FROM Sys_UserRole ur JOIN Sys_RolePermission rp ON ur.RoleId rp.RoleId JOIN Sys_Permission p ON rp.PermissionId p.Id WHERE ur.UserId userId AND p.PermissionCode IS NOT NULL查询结果写进 Session以 HashSet 保存后续每个请求判断权限时只做哈希查找成本可以忽略。DISTINCT 不能省用户可能挂多个角色不同角色可能绑定同一个权限码去掉去重会出现冗余数据权限集合变大后性能也会下降。2.4 菜单维护与权限码分配如何联动Sys_Permission 表本身就是菜单维护页的数据源权限管理后台通常就是对它做 CRUD 加树形展示。实际框架会预留少量系统级权限码比如 sys:menu:add、sys:role:edit用来控制权限管理页面自身的可见性和操作权业务菜单的权限码由二次开发人员按约定录入。新增业务模块时只要先在资源表里记录菜单和按钮再在对应 Controller 方法上标注相同 PermissionCode权限体系随即生效框架本身的过滤逻辑完全不用动。3. 认证与请求拦截ASP.Net MVC 管线和过滤器3.1 ASP.Net MVC 工作原理权限校验放在哪一环把 ASP.Net MVC 的请求链路展开就很容易理解为什么框架把权限校验放在过滤器里。请求进入后先由 UrlRoutingModule 接管RouteTable 里的路由模板把 URL 解析成 controller、action 和路由参数随后 MvcHandler 创建 Controller 实例然后进入过滤器管线。管线的顺序是 Authorization、Action、Result、Exception 四层权限校验属于 Authorization 阶段此时模型绑定尚未开始拦截一个不具备访问权的请求开销最小。常见的做法是新建一个继承 AuthorizeAttribute 的权限特性在 Authorization 阶段既检查登录态又检查当前用户是否拥有 Action 上标定的权限码。登录注册功能在 MVC 里也遵循同一套流程登录页面提交表单后Action 里校验账号密码通过后写 FormsAuthentication 票据再在本次 Session 中装入权限码集合。3.2 使用 PermissionAuthorizeAttribute 做控制器与 Action 级拦截一个可以直接用在控制器上的权限特性大致是下面这样public class PermissionAuthorizeAttribute : AuthorizeAttribute { public string PermissionCode { get; set; } protected override bool AuthorizeCore(HttpContextBase httpContext) { // 先交给基类判断是否已登录 if (!base.AuthorizeCore(httpContext)) return false; // 登录成功后权限码集合放进 Session var permissions httpContext.Session[UserPermissions] as HashSetstring; if (permissions null) return false; // 特性上没标注权限码表示只需登录 if (string.IsNullOrEmpty(PermissionCode)) return true; return permissions.Contains(PermissionCode); } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { // 已登录无权限与未登录要区分处理 if (filterContext.HttpContext.User.Identity.IsAuthenticated) { filterContext.Result new ViewResult { ViewName Error403 }; } else { base.HandleUnauthorizedRequest(filterContext); } } }这段代码里三个细节值得展开。继承 AuthorizeAttribute 后基类的 AuthorizeCore 已经处理了 FormsAuthentication 的票据校验过滤器里不需要重复解析加密 Cookie。权限码集合以 HashSet 存在 Session 里从构造上杜绝重复项也把判断逻辑从遍历字符串变成常数时间操作。HandleUnauthorizedRequest 必须区分未登录和已登录无权限两种情况未登录跳转登录页已登录无权限返回 403 页面否则用户会被反复踢回登录页却不知道自己到底缺哪个权限。使用方式是把特性放在控制器类级别或单个 Action 上[PermissionAuthorize(PermissionCode sys:user:add)] public ActionResult Create() { return View(); } [PermissionAuthorize(Roles Admin)] public ActionResult Delete(int id) { // ... }注意基类自带的 Roles 属性可以继续用它在角色层面判断业务权限码走 PermissionCode在权限层面判断。两者不要混成一套字段。角色判断适合少数硬编码的系统管理员权限码判断适合业务模块按资源细分授权。提示不要同时在 Session 里存角色列表和权限码列表。权限码由角色推导而来只存一份即可否则两个列表更新时机不一致会出现“角色已变但权限码没变”的诡异问题。3.3 路由注册、POST 请求读取和 Session 一致性MVC 的路由方法配置常见做法是在 RouteConfig 里按优先级注册。如果项目启用了 Area区域路由必须先在默认路由之前注册否则子模块的 URL 会被通用规则抢先匹配控制器上的权限特性跟着失效public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute({resource}.axd/{*pathInfo}); routes.MapRoute( name: AreaDemo, url: Admin/{controller}/{action}/{id}, defaults: new { controller Home, action Index, id UrlParameter.Optional } ); routes.MapRoute( name: Default, url: {controller}/{action}/{id}, defaults: new { controller Home, action Index, id UrlParameter.Optional } ); } }路由注册顺序的问题在框架集成阶段最常见默认路由一旦抢在 Area 前Controller 名相同就会报二义性用户可能被带到错误的模块权限校验自然不生效。另一个容易忽略的点是在过滤器或日志模块里读取请求体。请求流是单向的如果用 StreamReader 读了 POST 内容而不复位后续模型绑定拿到的就是空数据using (var reader new StreamReader(httpContext.Request.InputStream)) { var body reader.ReadToEnd(); // 复位到起点模型绑定才能再次读取请求体 httpContext.Request.InputStream.Position 0; }排查“权限日志正常但 Action 接收的 model 为 null”时先看这里有没有读过 InputStream。除此之外Session 的分布式场景也值得检查后端部署了多台 IIS 时默认 InProc 模式各自保存 Session用户被负载均衡到另一台机器时权限集合就丢了。这种情况要把 Session 切换到 StateServer、SQLServer 或 Redis权限过滤才会稳定。升级到 ASP.Net Core 时这套 RBAC 表结构和过滤器职责可以原样平移只是 Session 默认不跨实例需要换成 IDistributedCache 保存权限码集合。4. 快速开发部分统一响应、通用仓储与代码生成4.1 统一响应模型是前后端约定的第一道契约快速开发框架里最不起眼但最值得先定下来的底座是统一响应模型。所有 Controller 方法返回同一结构前端 Ajax 层只需要判断 Code 一个字段不用每个接口各自定义返回格式。常规写法public class ApiResultT { public int Code { get; set; } // 0 成功非 0 业务失败 public string Msg { get; set; } public T Data { get; set; } public static ApiResultT Success(T data) { return new ApiResultT { Code 0, Msg ok, Data data }; } public static ApiResultT Fail(string msg) { return new ApiResultT { Code 1, Msg msg }; } }定义好之后业务代码的写法可以很统一public ActionResult Save(Sys_Role role) { if (!ModelState.IsValid) return Json(ApiResultint.Fail(角色信息不完整)); _roleRepo.Update(role); return Json(ApiResultint.Success(role.Id)); }前端在 success 回调里只需要判断 result.Code 0参数校验失败、业务异常都不再需要额外约定状态码。Code 的取值要在框架内部集中定义比如 0 成功、1 业务失败、401 未登录、403 无权限不要和 HTTP 状态码混为一谈。4.2 通用仓储与服务层的边界快速开发脚手架的第二块是通用仓储。它不追求消灭 SQL而是把实体表的增删改查统一收口方便将来在入口统一加审计日志、软删除过滤或默认值赋值。用 Entity Framework 时一个精简实现如下public class RepositoryT where T : class { protected readonly DbContext _db; public Repository(DbContext db) { _db db; } public IQueryableT Query() { return _db.SetT(); } public T GetById(int id) { return _db.SetT().Find(id); } public void Insert(T entity) { _db.SetT().Add(entity); } public void Delete(T entity) { _db.SetT().Remove(entity); } public void SaveChanges() { _db.SaveChanges(); } }方法职责的边界在表里列出来更直观方法职责Query()返回查询入口配合 Where、Select、Include 使用GetById()按主键取实体命中 EF 一级缓存Insert()将实体标记为 Added不立即落库Delete()将实体标记为 Deleted等待统一提交SaveChanges()提交全部变更维护工作单元语义Insert、Delete 只是改变上下文状态真正落库统一走 SaveChanges工作单元的控制权放在 Service 层或 Controller 的 using 范围内。不要把 SaveChanges 散落在每个方法里否则一个业务操作涉及多张表时很难保证事务一致性这是初级框架最容易出现的问题。复杂查询不建议继续套泛型仓储。单表标准 CRUD 用 Repository 顺手多表 JOIN、汇总统计用 Dapper 直接写 SQL 或建数据库视图更直观。给这类权限管理框架做二次开发时不要指望一个 Repository 解决全部数据访问问题。4.3 代码生成器反射数据库结构生成 Controller 壳快速开发的第三部分是代码生成它解决的不是“复杂逻辑写不出来”而是“重复的 Controller 壳太多”。常见做法是读取数据库系统表里的列信息按模板拼接字符串批量生成实体类、Service 骨架和 Controller 骨架。一个最小实现片段var tables db.Querystring( SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPEBASE TABLE ).ToList(); foreach (var table in tables) { var columns db.Query( SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME t, new { t table }).ToList(); var sb new StringBuilder(); sb.AppendLine($// {table} 由代码生成器生成允许手工修改); sb.AppendLine($public class {table}Controller : Controller); sb.AppendLine({); sb.AppendLine($ private readonly Repository{table} _repo;); sb.AppendLine($ public {table}Controller(Repository{table} repo)); sb.AppendLine( {); sb.AppendLine( _repo repo;); sb.AppendLine( }); sb.AppendLine(}); File.WriteAllText(Path.Combine(outputDir, ${table}Controller.cs), sb.ToString()); }代码生成器的关键是“生成之后还可以改”所以生成文件头部要有标记比如“允许手工修改”并把生成目录和手写目录分开。实际维护中一般准备两套模板一套给单表无关联的简单页面生成完可直接运行另一套给主子表复杂页面只生成方法骨架和路由业务逻辑由开发者补全。这样速度保住了项目也不会被生成的代码绑死。5. 简洁美观的代价动态菜单、按钮权限与缓存刷新5.1 菜单由权限表驱动布局由母版页统一简洁美观对这个框架来说主要不是视觉特效而是入口不散落。左侧菜单树由 Sys_Permission 资源表动态渲染母版页读取当前用户菜单并按 Sort 排序输出。菜单维护页改一条记录用户刷新后即生效不用改页面代码重新发布这是后台类项目最实用的“美观”。5.2 按钮权限需要前端隐藏加后端拦截双重发力只做后端拦截界面上会出现一堆点不动的按钮只做前端隐藏直接拼 URL 一样能调接口。给 HtmlHelper 写一个扩展方法可以统一控制public static MvcHtmlString IfPerm(this HtmlHelper html, string code, string snippet) { var perms html.ViewContext.HttpContext.Session[UserPermissions] as HashSetstring; return perms ! null perms.Contains(code) ? new MvcHtmlString(snippet) : MvcHtmlString.Empty; }视图里写Html.IfPerm(sys:role:delete, button删除/button)即可。界面上的隐藏只是体验处理后端对应 Action 的 PermissionAuthorize 特性不能省。5.3 权限变更后的缓存刷新推荐版本号方案Session 里缓存权限码会遇到一个现实问题管理员改了角色权限在线用户的权限码还是旧的。处理方式有三类方案做法适用场景强制重登权限变更后清 Session让用户重新登录权限变更极少的内网后台版本号比对用户表加 PermissionVersion变更时递增过滤器不一致就重载需要即时生效的管理系统每次查库每次请求到 Action 前临时查权限表权限粒度细、变更频繁、用户量不大个人推荐版本号方案。用户登录时把 Version 放进 Session权限变更事务里对相关用户 Version1过滤器对比版本号不同才重新查询权限码并更新 Session。用户无需重新登录数据库也不会被每次都查。把版本号递增和 Sys_RolePermission 写入放同一个 SaveChanges 提交权限即时生效的链路就完整了。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门