.Net6.0前后端分离权限管理框架:RBAC、JWT与代码生成器实战
简介基于.Net6.0的权限管理及快速开发框架采用前后端分离架构适合C#/.NET开发者快速搭建企业级后台系统也可作为中小项目的技术底座。框架内置组织机构、角色用户、权限授权、多系统与多应用管理、定时任务、业务单据编码规则、代码生成器等核心模块并整合ASP.NET Core MVC、EF、Dapper、WebAPI、Swagger、Vue等主流技术架构易于扩展能显著缩短开发周期。压缩包共1332个文件大小约5.8MB其中533个C#源文件承载后端业务138个Vue组件配合180个JS文件及SCSS/CSS样式实现前端界面另附SQL脚本、csproj配置等便于直接还原项目环境。目前已有1277人学习下载。从预览文件可见包含Yuebon.Commons、Yuebon.AspNetCore等工程及BaseRepository、BaseService、WxOpenController等基础类可学习仓储模式、服务层设计、微信对接等实践配合代码生成器与权限授权机制能帮助开发者复用框架快速产出项目。1. .Net6.0前后端分离的权限管理框架为什么我最终选了这个方向一个新项目上来就要求“带用户管理、角色管理、菜单权限还要前后端分离一个月内交初版”很多团队的起步动作是写登录、写JWT、写路由守卫、写动态菜单一路忙完才发现核心业务还没碰。这里说的“基于.Net6.0的权限管理及快速开发框架前后端分离.zip”就是这类框架最常见的一种打包形态后端是 .Net6.0 Web API前端是 Vue 单页应用数据库脚本、RBAC 权限模型、代码生成器都给你备好。它能解决的是重复造轮子的问题适合要快速交付企业内部后台、又不甘心用传统单体模板的团队。这套方案不是银弹但它能帮你把“权限”和“通用后台功能”从项目启动清单里划掉让你把时间花在业务表结构、业务规则和交付质量上。下面我按自己的落地习惯从权限模型、启动步骤、二开到避坑一层层说清楚。2. 先看懂框架的分层RBAC权限模型在.Net6.0里怎么落2.1 菜单表和角色表的经典RBAC设计这类框架的权限模型基本都走 RBAC也就是用户—角色—权限三层关系。核心表一般就五张sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu。如果你用过若依框架前后端分离那套表结构再看这里会特别眼熟因为大多数快速开发框架的表设计都借鉴了同一套业务习惯。下面是建库脚本里最常见的一段省略了业务字段只保留权限相关主结构CREATE TABLE sys_user ( user_id BIGINT PRIMARY KEY, username NVARCHAR(50) NOT NULL, password NVARCHAR(100) NOT NULL, status INT DEFAULT 1, dept_id BIGINT ); CREATE TABLE sys_role ( role_id BIGINT PRIMARY KEY, role_name NVARCHAR(50) NOT NULL, role_key NVARCHAR(50) NOT NULL, data_scope INT DEFAULT 1 ); CREATE TABLE sys_menu ( menu_id BIGINT PRIMARY KEY, parent_id BIGINT DEFAULT 0, menu_name NVARCHAR(50) NOT NULL, menu_type CHAR(1), -- M目录 C菜单 F按钮 perms NVARCHAR(100), -- 权限标识例如 system:user:add path NVARCHAR(200), component NVARCHAR(200), visible INT DEFAULT 1, status INT DEFAULT 1, order_num INT DEFAULT 0 ); CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL ); CREATE TABLE sys_role_menu ( role_id BIGINT NOT NULL, menu_id BIGINT NOT NULL );这里最容易被新手忽略的是sys_menu里的menu_type和perms。目录、菜单、按钮全放在一张表里menu_type用 M、C、F 区分按钮权限的 perms 会写成system:user:add这种三段式。后端做接口鉴权、前端做按钮显隐都靠这一串字符串做匹配。主键类型建议用 bigint 且不要依赖数据库自增因为代码生成器一旦要批量导入菜单种子数据自增主键跨库迁移会非常头疼。用业务可读的固定 ID 或雪花 ID 都能少踩一个坑。2.2 动态权限点在 .Net6.0 里的落点策略授权与 JWT Claim后端的权限判断重点不是写一堆 if 判断而是把“当前用户有哪些 perms”放进 JWT Claim再用 ASP.NET Core 自带的策略授权做收口。这样业务代码里只需要在 Controller 上标[Authorize(Roles admin)]或自定义策略不用到处查角色表。典型配置写在Program.cs里builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidIssuer YourIssuer, ValidateAudience true, ValidAudience YourAudience, ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(YourSecretKeyMustBeLongEnough)), ValidateLifetime true, ClockSkew TimeSpan.FromSeconds(30) }; }); builder.Services.AddAuthorization(options { options.AddPolicy(permission:system:user:add, policy policy.RequireAssertion(context { var userPermissions context.User.FindAll(perms) .Select(c c.Value).ToHashSet(); return userPermissions.Contains(system:user:add); })); });这段代码里FindAll(perms)读取的是 JWT 里放进 payload 的permsclaim 列表。生成 token 时通常会先把用户拥有的所有权限点查出来塞进 claim一次登录后续接口都从 token 里取不再反复查库。ClockSkew那个参数值得单独说。默认五分钟的偏移意味着 token 明明过期了还能“多活”五分钟。我把线上 .Net6.0 框架的时间偏移改成 30 秒才避免客户说“账号刚过期又在灵异访问”的奇怪抱怨。2.3 前端路由与按钮权限的联动方案前端不能把业务路由写死在router.beforeEach里否则新增菜单只能重新发版。常见做法是登录后先请求后端菜单接口拿到树形菜单数据再动态注册 vue-router 路由。下面的代码是一个能跑的最小版本const modules import.meta.glob(../views/**/*.vue); function buildRoutes(menus) { return menus .filter(m m.menuType C) // 跳过目录和按钮 .map(m ({ path: m.path, name: m.component || m.path, component: modules[../views${m.component}.vue], meta: { title: m.menuName, perms: m.perms }, children: m.children ? buildRoutes(m.children) : [] })); } router.addRoute({ path: /, component: Layout, children: buildRoutes(userMenus) });import.meta.glob是 Vite 提供的按需加载方式它会返回一个对象key 是文件路径value 是组件加载函数。菜单接口里的component字段写的是system/user/index拼上前缀就能对应到物理文件这要求后端填 component 时必须和前端views目录结构完全一致。按钮权限的联动则用自定义指令做。比如注册一个v-permissionsystem:user:add指令内部判断当前用户拥有的 perms 集合没有就移除对应 DOM。这样业务页面只需要写指令不用每个组件里都手动 v-if 判断。3. 把框架跑起来从 zip 压缩包到前后端联调的最小步骤3.1 解压后先做项目结构识别一眼分清后端、前端、数据库脚本拿到压缩包先别急着运行。这类框架的目录通常很有规律用 tree 命令一眼能看清tree -L 2 -d常见结构是backend/、frontend/、sql/三个顶层目录。backend下面一般直接放.sln解决方案文件frontend下面有package.jsonsql里放着建库脚本和种子数据脚本。如果你看到docker-compose.yml说明还带了基础中间件编排本地调试会省很多事。有一个判断点确认后端是 .Net6.0 项目的标志是*.csproj里有TargetFrameworknet6.0/TargetFramework而不是目录名带个 6 就被当成 .Net6.0。看到 global.json 也要留意它可能固定了 SDK 版本和本机不一致会导致还原失败。3.2 后端启动修改连接字符串与初始化种子数据后端启动第一步是改appsettings.json里的连接字符串。最常见的坑是数据库实例名、密码带特殊字符导致连接失败。建议先用 Sql Server Management Studio 建好空库再执行 sql 目录下的脚本而不是让框架自动建库。{ ConnectionStrings: { Default: Serverlocalhost;DatabaseAdminFramework;User Idsa;PasswordYourPass;TrustServerCertificatetrue }, AppSettings: { SeedAdmin: true, DefaultAdminUser: admin, DefaultAdminPassword: admin123 } }连接字符串里TrustServerCertificatetrue在 .Net6.0 中很关键用的 SQL Server 如果没配正式证书握手会直接失败。SeedAdmin这个配置项控制启动时是否自动写入管理员账号和初始菜单数据第一次运行建议开着后续环境第一次部署也开着可以少手工导一遍。修改完成后在backend目录下执行dotnet restore dotnet run看到控制台输出监听地址为http://localhost:5000后先用curl http://localhost:5000/health试探一下健康检查接口。如果返回 200说明后端进程起来了接下来再处理数据库是空库导致的报错。3.3 前端启动代理配置与本地调试前端一般跑在 5173 或 8081 端口后端在 5000 端口直接用 axios 请求一定是跨域。常见做法不是后端配 CORS 放行而是在 Vite 代理里把/api转发到后端地址。// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:5000, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } });这里的rewrite字段要注意很多框架的 Controller 路由本身就带api前缀如果又配了 rewrite就会出现请求到了后端却定位不到 Controller 的问题。先看后端 Swagger 里的接口地址带不带/api再决定要不要 rewrite。我第一次搭这套环境时就是前端 proxy 加了 rewrite结果登录接口一直 404。前端依赖安装用npm install npm run dev安装速度慢的话把 registry 换成国内镜像再跑能省不少时间。3.4 用 Swagger 和登录页验证端到端联调后端启动后访问http://localhost:5000/swagger应该能看到一组鉴权相关接口。先找一个登录接口测试拉通后端能力curl -X POST http://localhost:5000/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:admin123}返回 JSON 里通常包含accessToken和用户信息。拿到 token 后再访问需要鉴权的接口请求头带上Authorization: Bearer token如果返回 200说明后端认证链路正常。前端验证分两步打开登录页用 admin/admin123 登录登录成功跳到首页然后看菜单栏是否有动态渲染出来的服务菜单。如果首页白屏打开浏览器控制台看有没有Cannot read properties of undefined这类报错多半是后端菜单接口里某个字段和前端约定不一致。4. 二开必看快速开发框架的代码生成器与业务接入4.1 代码生成器的三个输入与一次生成产物快速开发框架的核心价值不是权限而是生成一套标准增删改查。生成器的输入通常只有三个数据库表名、界面模板类型、生成模块名称。以一张业务表为例CREATE TABLE biz_order ( order_id BIGINT PRIMARY KEY, order_no NVARCHAR(50) NOT NULL, customer_name NVARCHAR(50), total_amount DECIMAL(18,2), status INT DEFAULT 1, create_time DATETIME );在生成器页面填表名biz_order选择“单表模板”确认生成框架会产出这些内容实体类BizOrder、BizOrderService、BizOrderController、前端views/biz/order/index.vue、api/biz/order.ts以及数据库里插入菜单的一条 SQL。有些框架还会生成 DTO 和分页查询对象不用手工建 controller 是基本能力。这里有个使用习惯生成代码后先不要直接跑打开生成的BizOrderController检查 Action 的标签是否被自动标注了[Authorize]。有些框架生成器默认只生成一个没有任何鉴权标签的控制器直接上线等于接口裸奔。4.2 给新业务模块挂菜单和权限点代码生成器产出的代码不会自动把菜单挂到系统里还需要在菜单表手工插入目录和按钮。明确一下一个业务模块通常需要三条菜单记录——上级目录、页面菜单、按钮权限。SQL 写法类似INSERT INTO sys_menu(menu_id, parent_id, menu_name, menu_type, perms, path, component, order_num) VALUES (2000, 0, 订单管理, C, NULL, /biz/order, biz/order/index, 1); INSERT INTO sys_menu(menu_id, parent_id, menu_name, menu_type, perms, path, component, order_num) VALUES (2001, 2000, 订单查询, F, biz:order:query, NULL, NULL, 1), (2002, 2000, 订单新增, F, biz:order:add, NULL, NULL, 2), (2003, 2000, 订单删除, F, biz:order:delete, NULL, NULL, 3);注意menu_id不要用数据库自增框架里菜单树经常用 parent_id 拼递归如果 id 没规律灌初始化数据时很容易给不同环境造成 id 分叉。perms三段式编码建议严格遵守模块名:功能:动作因为它会被后端Authorize策略和前端v-permission指令同时用来匹配。最隐蔽的坑是菜单里path必须和前端路由 path 一致component必须和控制台里动态 import 的文件路径一致。框架不会替你做两遍所以插入菜单后记得刷新页面看路由是否正常加载。4.3 角色数据权限从“自己可见”扩展到“部门可见”很多业务不只要求“谁能访问”还要求“同一角色能看到哪些数据”。常见做法是把sys_role加一个data_scope字段1 全部数据2 自定义部门3 本部门数据4 仅本人数据。后端在处理列表查询时根据当前角色的data_scope拼接过滤条件。var query _db.BizOrders.Where(o o.IsDeleted false); if (dataScope 3) // 本部门 { query query.Where(o o.CreateDeptId currentDeptId); } else if (dataScope 4) // 仅本人 { query query.Where(o o.CreateUserId currentUserId); }这里逻辑看起来很简单但第一个翻车点在于CreateDeptId是否能从当前用户的 JWT 里拿到。建议登录时在 token 里带上deptId、userId两个 claim否则数据权限的代码会为了拿到部门 ID 又去查一次用户表开销和复杂度都会上来。第二个翻车点是部门表通常有上下级本部门及以下部门不是简单等值匹配。如果框架的部门表没有递归查询封装你要么用公共表表达式CTE递归找子部门要么用物化路径。至少我接触到的几个基于 .Net6.0 的后台框架默认只支持“本部门”客户一旦提出“经理要看整个部门树的数据”坑就来了。5. 避坑清单这类框架最常见的翻车点现象、原因、解决5.1 前端一直 401后端明明能看到登录接口现象用 admin 登录成功但随后任何一个业务接口请求都是 401后端日志里能看到“未授权”异常。原因大多是前后端对 token 的存储和提交不一致。前端 Vue 代码可能把 token 放在了 localStorage 里叫token而 axios 拦截器里读的是access_token或者请求头拼成了Authorization: token xxx后端只认Bearer前缀。解决把 axios 请求拦截器统一改成从固定存储键读取 token并强制进行 Bearer 拼接。同时检查后端AddJwtBearer里TokenValidationParameters是否关闭了ValidateAudience或ValidateIssuer不一致导致的有效性校验失败。service.interceptors.request.use(config { const token localStorage.getItem(accessToken); if (token) { config.headers[Authorization] Bearer ${token}; } return config; });这个坑往往不是框架缺陷而是每个二开者都会习惯性改前端存储变量名忘了同步修改。养成一个习惯登录后统一截获登录接口的返回体把 token 写入锁定键名后续全项目只认这一个键名。5.2 生成器建的表没有主键EF Core 直接报错现象代码生成器生成实体后运行到列表查询接口后端异常提示“The requested operation requires an entity of a certain type that does not have a primary key”。原因数据库源头表没有显式主键代码生成器读取表结构时拿不到主键元数据生成的实体类缺少[Key]标注。EF Core 要求实体必须有主键才能执行跟踪查询没主键的表即使在 SQL Server 里能打开一接入 ORM 就崩。解决在建业务表时先确认主键字段名比如统一用xxx_id作为主键并设置 PRIMARY KEY。如果表已经存在补上主键再重新在生成器里加载一次。这里建议直接用明确的主键名称而不是让 EF 默认推断Id减少歧义。我踩过一次更稳的坑表里有复合主键生成器只识别了第一个字段导致删除接口只按部分主键删除最终删错数据。复数主键的业务表不要依赖框架生成器手工写实体时用[Key]配合[Column]才是保险的。5.3 动态路由刷新页面白屏现象退出登录后用新账号登录菜单能正常渲染但一刷新页面就白屏控制台报路由匹配不到。原因动态注册的 vue-router 路由只在登录后 addRoute页面刷新时会重新初始化整个应用路由表是空的router.beforeEach还没执行完就直接跳到了重定向逻辑导致找不到匹配组件。解决在beforeEach里判断 store 中是否已经有菜单数据如果没有就先请求菜单接口再router.addRoute(route)之后充分使用next({ ...to, replace: true })让这一次导航重新触发。核心是保证动态路由注册完成后再进入业务路由。这个问题的判断点是刷新后代码在哪个阶段报错。如果你在控制台看到 “Match failed” 就是在 addRoute 前路由导航已经发生。也可以用router.hasRoute()做防御但最稳妥的仍是把菜单加载动作收敛到路由守卫里。5.4 JWT 过期时间设置成 8 小时客户说“第二天又掉线”现象本地调试业务接口正常部署到客户环境第二天全员登录状态被清掉。原因这不算故障而是 JWT 无状态的自然结果。框架可能把过期时间写死成 8 小时内部系统一般工作时间是 9 小时以上到了下午后半段 token 就过期了。而客户认定的“掉线”是登录页面被顶出前端没有做 token 过期自动刷新用户只能重新登录。解决先看生成 token 时expires参数一般是两套方案。要么把 token 有效期延长到 12 小时并配一个刷新 token要么实现刷新 token 机制在 axios 响应拦截器里识别 401拿到 refreshToken 静默换取新 token再重放失败请求。快速破局做法是先把有效期调到 8 小时加一个前端自动刷新流程后续再做双 token。需要注意 JWT 携带在Authorization头而不是 URL query 里不然在网关日志或浏览器历史里会留下明文 token这也是权限框架最常见的泄露通道。5.5 代码生成器覆盖了手改的 Service 代码白干一下午现象对生成的BizOrderService手工加了一个统计方法后来又为了新表再次执行生成器文件被整体覆盖手写方法消失。原因很多快速开发框架里的“按表重新生成”对单个表是全量覆盖模式并不会做代码合并。同一个表反复调整业务功能重新生成等于把自己改动的文件全部还原。解决使用代码生成器时先区分“首次生成”和“二次生成”。首次生成后把生成的业务文件复制到独立的业务目录每次对表结构变更尽量手工修改实体和 Service而不是重新套模板生成。若是必须重新生成当前表的所有手工改动先提交到 git再对照差异把迁移逻辑合回来。这个坑属于使用时序问题不是框架本身编程 bug所以不指望生成器变聪明而是在团队内部定一个规矩不被生成器直接改动的文件单独放一个custom/目录业务代码入口只是调用它这样重生成也不会把你写的逻辑吞掉。6. 进阶把权限审计带上线的验证方法与性能优化权限功能上线前我最常用一套轻量冒烟脚本来验证权限边界是否真的生效。准备三个账号admin全部权限、ops部分权限、readonly仅查看然后循环访问所有已配置的接口期望返回 401 或 403 的接口坚决不能出现 200否则就说明权限点漏配。这个验证脚本可以写成本地 SQL 加上简单 curl 循环核心是“用表格记录每个接口所需权限标识再和角色关联表交叉比对”。验证代码的关键是让断言清晰举一个极小的做法用 PowerShell 脚本读取已授权接口清单逐个带上只读角色的 token 发起请求将所有返回 200 但本应 403 的接口输出到一个unauthorized-report.csv文件。留下报告再走审批流程比手工测试再让测试点几百个按钮高效得多。性能方面框架最容易出现的瓶颈是每个请求都在解析菜单树和权限集合。常见优化是把“用户权限集合”放进内存缓存键是userId首次登录查库之后从缓存读取。菜单树也一样因为菜单改变频率很低启动时加载一次缓存即可。我在一个并发接单的小后台里把每请求两到三次的权限查库改成一次缓存读取后接口 P95 延迟从 220ms 掉到了 90ms。另一个优化点很隐蔽JWT 的 payload 里不要堆大量权限标识。假设一个用户有 300 个按钮权限每个 perm 平均 25 字符加上 Claims 编码后的体积就会让每次请求的 Authorization 头膨胀到几 KB网关和反代都会受影响。建议只保留一个端点信息业务方在需要时再通过一个内部接口获取详细权限。这里容易冲动但先看线上 token 大小再决定要不要优化也不算玄学。我吃过一次亏为图省事把部门信息、岗位、权限列表全部塞进 token结果客户网络环境里代理层对请求头长度做了限制导致登录后疯狂 401。后来把所有重信息挪到缓存只保留 userId 和 core 权限标识问题才彻底消失。从那以后我对任何框架的“自定义 Claim”都抱着最小化原则。权限这层不是越厚越好边界清晰、可审计、可缓存才是长期能养的东西。希望帮到你。本文还有配套的精品资源点击获取