多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

.NET 6前后端分离权限管理框架:RBAC、JWT与快速开发实战

.NET 6前后端分离权限管理框架:RBAC、JWT与快速开发实战 简介这是一套基于.Net6.0的前后端分离权限管理及快速开发框架定位面向中小型项目的开发者与技术选型者用于解决通用权限、组织人员、定时任务等基础模块的重复建设问题。核心模块涵盖组织机构、角色用户、权限授权、多系统/多应用管理、定时任务、业务单据编码规则及代码生成器底层整合Asp.Net Core MVC、EF、Dapper、WebAPI、Swagger与Vue等主流技术整体架构清晰、易于扩展。压缩包共1332个文件约5.8MB以533个cs后端源码、319个svg图形资源、180个js与138个vue前端文件为主体另含少量数据库脚本与工程配置文件可直接展开后进行二次开发。当前已有1277人学习下载。借助这份资料可以快速理解权限模型的落地方式、多应用隔离思路与代码生成机制并复用仓储层、服务层和前端基础组件显著缩短新项目的基础搭建周期。1. 拿到 .Net6.0 前后端分离权限管理框架先搞清楚它能替你省多少重复代码拿到一套基于 .Net6.0 的前后端分离权限管理及快速开发框架多数人第一反应是先把压缩包解压、跑起来看看登录页、菜单、角色配置是不是开箱即用。这类框架解决的问题非常具体不用再从零写用户登录、角色管理、菜单管理、接口鉴权这套所有中后台系统都绕不开的公共部分业务模块往现成壳子里挂就行。适合正在做 .NET 中后台项目、又不想被权限细节拖住进度的从业者。下面按我拆这类项目的顺序讲先看结构再跑通前后端把权限链路说透最后落到避坑和加新模块的验证路径。2. 框架结构拆解先看后端三层与前端工程再决定改哪里2.1 后端分层Controller 薄、Service 厚、Repository 只碰数据压缩包解压后第一件事不是急着启动而是先看解决方案结构。这类框架的后端基本逃不出四层启动项目Api、业务逻辑Application/Service、数据访问Repository/Infrastructure、公共基础Model/Common。层与层之间单向引用Api 引用 ServiceService 引用 RepositoryModel 被各层共用。项目/目录职责常见类Api控制器、过滤器、Swagger、JWT 鉴权配置Controllers/LoginController.cs, Program.csApplication/Service业务逻辑、DTO、事务管理Services/LoginService.csRepository/Infrastructure数据访问、仓储接口、ORM 封装Repositories/UserRepository.csModel/Common实体、枚举、通用返回包装Models/User.cs, Result.cs一个登录接口的完整调用链最能说明分层价值LoginController 只接收参数参数合法性交给模型状态校验LoginService 里做用户名密码校验、生成 JWT、写登录日志UserRepository 只负责按用户名查用户。如果看到控制器里直接new Repository()或者直接拼 SQL说明这个框架把分层当摆设后期改起来会痛。我一般用“Controller 十行以内”作为快速判断标准。// 控制器只做入参接收和结果返回业务判断全部下沉到 Service [HttpPost(login)] [AllowAnonymous] public async TaskResult Login(LoginDto input) { // ModelState 校验失败时请求不会进 Service避免业务层到处写 if if (!ModelState.IsValid) return Result.Fail(参数不合法); return await _loginService.LoginAsync(input); }入参校验这块LoginDto 上的[Required]、[StringLength]这类数据注解会被框架自动执行校验不过直接返回 400逻辑很直观。Service 里返回值统一走Result包装前端拿 body 里的 code 字段判断成功失败而不是依赖 HTTP 状态码。这套约定不少权限框架都采用好处是业务异常也能走 HTTP 200 返回前端处理统一。框架里_loginService来自构造函数注入生命周期常见配置是 Scoped意味着同一个 HTTP 请求内拿到同一个实例事务可以跨多个 Repository 共享。如果配成 Singleton遇到 EF Core 或 SqlSugar 的并发上下文会踩线程安全坑。2.2 前端工程Vue 管理后台的目录约定与请求封装后端看完看前端。这套框架的前端一般是 Vue Element 系具体是 Vue 2 Element UI 还是 Vue 3 Element Plus打开 package.json 看 vue 版本字段就能确认两者在目录组织上没有本质差别。真正要关注的是三块请求封装、动态路由、页面目录。路径职责src/utils/request.jsaxios 实例请求拦截器带 token响应拦截器统一处理错误src/api/login.js登录、登出、获取当前用户信息src/router/index.js静态路由 动态路由生成逻辑src/store/modules/user.js保存 token、用户信息、菜单权限请求封装是所有前后端分离项目的命门这套框架里通常长这样// src/utils/request.js 的 axios 拦截器所有请求都会走到这里 service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( res { // 业务层统一返回的 code401 表示登录态失效 if (res.data.code 401) { localStorage.clear() window.location.href /login } return res.data }, err { // HTTP 层 401 通常是 token 过期或签名错误 return Promise.reject(err) } )这套封装是多数中后台的标配写法请求拦截器统一把 localStorage 里的 token 塞进 Authorization 头响应拦截器统一处理后端业务码。注意 HTTP 401 和业务码 401 是两套东西JWT 过期通常返回 HTTP 401业务校验失败是响应体里的 code 字段具体以框架约定为准。前端 401 跳登录页只是体验兜底真正的权限判断必须以后端为准这是拆这类项目时最需要守住的原则。前端把菜单隐藏了接口没拦住一样是安全事故。2.3 技术选型为什么值得用.NET 6 LTS、JWT、ORM 的取舍为什么这类快速开发框架集中选 .NET 6 而不是老掉牙的 .NET Framework因为 .NET 6 是 LTS 版本从 2021 年发布到 2024 年 11 月官方支持期都很充足部署到 Windows 服务器或 Linux 容器都有成熟方案。虽然现在新项目已经有人直接上 .NET 8但存量框架和这类脚手架选 .NET 6 仍然是稳妥选择——生态组件兼容性好网上踩坑记录多出问题能搜到答案。方案优点缺点适合场景.NET Framework 4.x老项目兼容好跨平台差、性能一般遗留系统维护.NET Core 3.1跨平台成熟已停止支持不推荐新项目.NET 6LTS 支持周期长比 .NET 8 旧存量框架、保守选型.NET 8性能更好、新特性多生态迁移中全新项目ORM 层面这类框架用 SqlSugar 的偏多因为“快速开发”四个字决定了开发效率优先。SqlSugar 语法接近 SQL分页、连表写起来快国内资料多EF Core 的迁移能力和 LINQ 表达更完善但约定比较多新手容易在导航属性上翻车。我的看法是如果你是改业务代码不用纠结 ORM跟着框架现有写法走。框架用 SqlSugar 你就写db.QueryableUser().Where(...)用 EF Core 你就写context.Users.Where(...)保持一致最重要。鉴权方案上前后端分离场景下 JWT 是绝对主流。Session 依赖 Cookie 和服务器状态跨端调用、移动端接入都不方便JWT 无状态校验时只验签名不查会话表服务端压力小。代价是注销和权限变更做不到实时一般配合 Redis 存用户权限版本来解决后面第 4 章会展开讲。3. 从零跑通数据库脚本、后端启动、前端联调一条线3.1 环境准备版本不对后面全是踩坑跑这套框架最少需要四样东西.NET SDK、Node.js、MySQL、Redis。版本上不需要追新稳定为主。我本机环境是 .NET SDK 6.0.x、Node 16 LTS、MySQL 8.0、Redis 6这套组合跑绝大多数权限框架都没问题。工具建议版本用途.NET SDK6.0.x编译、运行后端项目Node.js14 LTS 或 16 LTS前端依赖安装与本地开发MySQL5.7 或 8.0业务数据存储Redis5.x 及以上缓存、验证码、权限缓存IDEVisual Studio 2022 或 Rider调试 C# 代码这里提前说一个新手常用的坑装了 .NET 8 SDK 的机器可以直接编译目标框架为 net6.0 的项目但运行时还是要装 6 的 runtime或者给 dotnet 配置 roll-forward。最常见的情况是本地编译通过发布到服务器后提示“You must install .NET runtime”第 5 章避坑部分再展开。检查版本用一条命令dotnet --list-sdks dotnet --list-runtimes看到 6.0.x 的 SDK 和 runtime 都在列表里就可以继续。如果只有 8.0也能编译 net6.0 项目但我会直接把两个运行时都装上省得后续发布时再来一遍。3.2 初始化数据库脚本执行顺序与连接串修改数据库脚本一般放在压缩包的 Databases 目录或 doc/db 目录下。脚本文件名如果带数字前缀就严格按顺序执行先建表结构再灌初始数据。不要跳着执行关联表和外键很依赖顺序。mysql -uroot -p --default-character-setutf8mb4 Databases/01_schema.sql mysql -uroot -p --default-character-setutf8mb4 Databases/02_data.sql第一条命令建表结构第二条灌初始数据。--default-character-setutf8mb4必须带否则中文乱码。执行完进入数据库检查两件事一是表的数量是否和文档对得上二是初始账号表里有没有数据。USE admin_db; SHOW TABLES; SELECT * FROM t_user LIMIT 5;如果表数量明显缺失基本可以断定脚本执行中断了重跑时先用 DROP 清掉已建的表再执行。如果 t_user 表是空的检查 02_data.sql 里是不是写死了数据库名连接账号有没有对应库的写权限。初始账号一般是admin/123456密码在数据库里是加密后的字符串不要试图手动改成明文后面验证登录还是要走后端加密逻辑。3.3 启动后端appsettings.json 三处必改参数后端启动之前先把 appsettings.json 里三处配置改掉数据库连接串、Redis 地址、JWT 密钥。这三处错任何一个启动能过但登录一定会出问题。{ ConnectionStrings: { Default: Server127.0.0.1;Port3306;Databaseadmin_db;Userroot;Password123456;Charsetutf8mb4 }, Redis: { Host: 127.0.0.1, Port: 6379, Password: }, Jwt: { Issuer: AdminApi, Audience: AdminApp, SecretKey: replace-with-a-key-at-least-32-chars, ExpiresMinutes: 120 } }连接串里 Server 用127.0.0.1比localhost更稳能避开部分环境下的 socket 解析问题。Charsetutf8mb4保证中文和 emoji 不被截断。Redis 的 Password 本机没设密码就留空字符串但生产环境必须设密码否则 Redis 默认无防护很容易被扫描爆破。JWT 的 SecretKey 至少要 32 个字符长度不足 HS256 签名算法会直接报错这个坑在第 5 章还会遇到。配置改完用命令行启动后端cd 你的解压目录/src dotnet restore dotnet run --project AdminApi/AdminApi.csprojdotnet restore恢复 NuGet 包第一次会慢一些看到“已还原”字样说明依赖拉取正常。启动后控制台会输出监听的地址一般是http://localhost:5000或https://localhost:5001。接着打开浏览器访问/swagger/index.html能看到接口列表就说明后端起来了。如果端口被占用去 Properties/launchSettings.json 里改 applicationUrl前后端代理里的 target 也要同步改。3.4 启动前端依赖安装、代理配置与首次登录后端跑起来后转到前端目录一般是 web 或 AdminWeb。安装依赖时公司网络很容易卡住直接指定镜像源cd web npm install --registryhttps://registry.npmmirror.comnode_modules 装完启动开发服务器npm run dev默认端口一般是 8080。打开浏览器访问http://localhost:8080这时候登录接口大概率还是调不通的因为前端开发服务器的/api请求需要代理到后端。代理配置在 vue.config.js 或 vite.config.js// vue.config.js 常用写法 module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } }changeOrigin: true会把请求头里的 Host 改成 target 地址避免后端做域名白名单时拒绝。改完代理配置必须重启前端热更新不覆盖 devServer 配置。这一步做完浏览器里用初始账号登录能进到首页看到菜单和用户信息前后端联调就算通了。如果提示密码错误去 t_user 表确认初始账号有没有被脚本改过密码或直接看 02_data.sql 里 INSERT 语句的明文。4. 权限是怎么生效的RBAC 表结构、JWT 链路与接口校验4.1 RBAC 数据模型五张表把用户、角色、权限串起来权限管理框架的核心是 RBACRole-Based Access Control基于角色的访问控制这套代码里落地为三张主表和两张关联表。理解这五张表整个权限体系就通了一半。表关键字段作用t_userid, username, password, status登录账号密码加密存储t_roleid, name, code, is_super角色is_super 标记超管t_menuid, parent_id, type, name, path, perm菜单 按钮权限标识t_user_roleuser_id, role_id用户和角色多对多关联t_role_menurole_id, menu_id角色和菜单/按钮权限多对多关联登录时后端做的事可以拆成三步通过用户名查用户拿 userId 去 t_user_role 查出角色再拿角色去 t_role_menu 查出菜单和权限码。一个用户可以有多个角色一个角色可以有多个菜单权限这就是“多对多”的含义。菜单表里的type字段通常区分目录、菜单、按钮三类目录只是导航层菜单是页面路由按钮是最细粒度的操作权限比如“新增客户”“删除订单”这种动作。权限校验的 bypass 逻辑也在这五张表里t_role.is_super 1的角色后端会直接跳过权限码比对所以超管能看到所有菜单、调所有接口。实际用的时候给内部运维账号开超管没问题给业务用户误开超管菜单全开只是一方面接口层全部放行才是隐患。我见过不止一次生产环境普通账号被挂上 is_super排查半天最后发现是初始化脚本里写死了角色编码。权限缓存一般会存进 Rediskey 设计为permission:{userId}用户改角色后要主动删掉对应缓存否则权限变更要等 Redis 过期才生效。4.2 JWT 登录签发与鉴权参数哪些值不能改错登录接口验证完密码后后端要做两件事生成 JWT 令牌把用户信息存 Redis 或直接编码进令牌。JWT 生成的常见写法// 登录成功后构造 Claims 和签名参数 var claims new ListClaim { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.UserName) }; var key new SymmetricSecurityKey( Encoding.UTF8.GetBytes(jwtSettings.SecretKey)); var credentials new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token new JwtSecurityToken( issuer: jwtSettings.Issuer, audience: jwtSettings.Audience, claims: claims, expires: DateTime.Now.AddMinutes(jwtSettings.ExpiresMinutes), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token);Claims 是 JWT 的“载荷”里面放什么有讲究。有的框架会把权限码列表new Claim(perms, string.Join(,, perms))塞进去好处是校验时不用查 Redis坏处是 token 变大、权限变更要等 token 过期才生效。另一派只在 token 里放 userId 和 name每个请求从 Redis 按permission:{userId}拉权限改角色立即生效。我拆过的框架里后者更常见对权限频繁调整的后台系统更友好。鉴权中间件配置决定了 token 怎么被校验builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidateAudience true, ValidateIssuerSigningKey true, ValidIssuer jwtSettings.Issuer, ValidAudience jwtSettings.Audience, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(jwtSettings.SecretKey)) }; });这里几个 Validate 开关在生产环境一个都不能关。ValidateAudience防止别人拿 A 系统的 token 调到 B 系统ValidateIssuerSigningKey保证 token 签名密钥正确。SecretKey 和登录签发时必须是同一个值改了密钥所有已签发 token 立即失效用户得重新登录。ExpiresMinutes 一般设 120 分钟左右太短影响体验太长有泄漏风险。有刷新令牌机制的框架会把刷新 token 的过期时间设到 7 天甚至更长没有刷新机制的就靠用户重新登录。4.3 接口级权限与动态菜单前后端如何对同一个权限码RBAC 落到代码层面是接口权限和页面权限两层。接口权限靠自定义特性标记[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public class PermissionAttribute : Attribute { public string Code { get; } public PermissionAttribute(string code) Code code; } // 控制器用法 [HttpGet(list)] [Authorize] [Permission(customer:list)] public async TaskResult GetCustomerList() { return await _customerService.GetListAsync(); }PermissionAttribute标记的customer:list就是 t_menu 表里某条权限记录的perm字段。拦截器或过滤器拿到当前用户的权限集合和customer:list比对没有就返回 403。这里最容易踩的坑是权限码不统一代码里写customer:list菜单表里存customer/list字符串格式对不上权限永远不生效。我在加字段时会把权限码当作接口文档的一部分前后端约定同一个字符串。前端动态路由是另一条线登录后根据用户菜单生成路由。router.addRoutes()拿到的是当前用户有权限的页面没权限的菜单压根不会注册。按钮权限一般通过自定义指令v-permissioncustomer:add控制没权限就移除按钮 DOM。但按钮隐藏只是用户体验接口必须用[Permission]拦住否则有人直接调接口就能绕过页面限制。前端拦截是面子后端校验是里子。5. 避坑指南环境、数据库、代理与权限的常见翻车现场5.1 数据库脚本执行到一半报错表建全了但数据灌不进去现象执行 01_schema.sql 提示Unknown collation: utf8mb4_0900_ai_ci后面脚本直接中断再跑就报“表已存在”。原因utf8mb4_0900_ai_ci是 MySQL 8.0 引入的排序规则MySQL 5.7 不认识。框架作者在 8.0 上导出脚本你的环境是 5.7字符集定义直接崩了。解决把脚本里所有utf8mb4_0900_ai_ci替换成utf8mb4_general_ci或者把本地数据库换成 MySQL 8.0。我习惯用版本对齐的方式和框架作者的数据库大版本保持一致后续尽量避免排序规则差异。改脚本前先备份用 sed 批量替换比手改快sed -i s/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g Databases/01_schema.sql5.2 后端启动即崩报 Redis 连接超时现象dotnet run之后几秒钟控制台抛出StackExchange.Redis.RedisConnectionException: It was not possible to connect to the Redis server(s)应用直接退出。原因框架登录、权限缓存、验证码都依赖 Redis本机没装 Redis 或配置的 Host/Port 不对。这是前后端分离框架最常见的环境缺失问题装了也没改 appsettings 里的 Redis 地址一样报错。解决本地快速起一个 Redis 容器比编译安装省事docker run -d --name redis -p 6379:6379 redis:6起来后检查appsettings.json里的 Redis Host 是不是127.0.0.1、Port 是不是6379改完重启后端。如果设置了 Redis 密码Password字段必须填否则鉴权握手失败。5.3 登录接口返回 401但账号密码确认是对的现象Swagger 调登录接口输入初始账号密码返回401 Unauthorized翻看后端日志没有业务异常。原因JWT SecretKey 长度不足。HS256 算法要求密钥至少 128 bit16 字节很多默认模板里写的123456这种短字符串运行时会静默失败或签名抛错。前端往往把这个错误当成用户名密码错误实际上和后端密钥配置无关。解决把 appsettings.json 里 Jwt.SecretKey 换成一个 32 字符以上的随机串改成和当前框架签发、校验共用同一份配置。改完重启后端重新登录。这里也提醒SecretKey 不要用容易被猜到的单词生产环境用 GUID 拼接或随机数生成器产出。5.4 前端代理配了还是 404登录请求打到前端自己头上现象浏览器控制台 Network 里看到请求地址是http://localhost:8080/api/login返回 404 或 504后端 Swagger 里完全看不到这条请求记录。原因vue.config.js 的 devServer.proxy 没生效。最常见两种一是改了配置没有重启 dev server热更新不覆盖 proxy二是代理匹配规则太严格后端接口前缀是/api没错但请求实际可能被 webpack 静态资源处理拦了或者框架里实际前缀是/api/v1。解决先重启前端再测还是不通就把代理匹配放宽proxy: { /api: { target: http://localhost:5000, changeOrigin: true, pathRewrite: { ^/api: } } }pathRewrite要不要配取决于后端路由前缀。如果后端控制器路由是[Route(api/login)]不用重写如果后端没有 api 前缀就必须把/api去掉再转发。看后端 Swagger 里的路径就一目了然。5.5 权限配置了但接口仍然 403按钮时隐时现现象角色菜单里已经勾选了某个按钮权限前端页面按钮也显示出来了但后端接口一直返回 403。原因权限码不一致或者权限缓存没有刷新。后端[Permission(customer:add)]的代码和 t_menu 表里存的perm字段差一个字符比对永远失败或者用户权限列表被缓存进 Redis角色绑定新菜单后没删缓存后端还在用旧权限集合做校验。解决第一步去数据库核对 t_menu 里那条记录的 perm 字段把它复制到代码特性里不要手动输入第二步找到管理后台“刷新权限缓存”的入口或者直接删 Redis 里的 keyredis-cli KEYS permission:* redis-cli DEL permission:{userId}从那以后我做权限联调时先确认权限码字符串完全一致再考虑缓存基本能避开这个坑。6. 进阶把一个新业务模块接进权限体系并验证整条链路6.1 最小模块代码与权限码绑定跑通框架后实际开发动作通常是往里面加业务模块。以一个“客户管理”为例最小闭环是三样东西实体表、后端接口、菜单权限记录。后端接口要做的不是写一大堆代码而是把权限码挂上去[Route(api/customer)] [ApiController] public class CustomerController : ControllerBase { private readonly ICustomerService _customerService; public CustomerController(ICustomerService customerService) { _customerService customerService; } [HttpGet] [Authorize] [Permission(customer:list)] public async TaskResult GetList() { // 实际查询逻辑放在 Service 里这里只做转发 return await _customerService.GetListAsync(); } }这里的customer:list就是权限码和 t_menu 表里 perm 字段、前端按钮指令里的值必须是同一个字符串。菜单记录插入后要挂到角色上才能看到菜单和按钮INSERT INTO t_menu(parent_id, type, name, path, perm, sort) VALUES (1, 1, 客户管理, /customer, customer:list, 9);插入后去角色管理界面给目标角色勾上这个菜单或者直接往 t_role_menu 插关联记录。我用 SQL 插入的方式做初始化正式环境还是建议走页面配置能少写不少关联查询。6.2 验证权限闭环普通账号 403绑定后 200验证路径要按“最小权限”原则走创建一个没有任何角色的普通账号登录后调GET /api/customer应该返回 403给这个账号绑定已勾选菜单的角色重新登录再调接口返回 200。注意重新登录和清缓存要同时做否则 Redis 里的旧权限还挂着验证结果不准。我在本地会额外删一次permission:*的 key确保权限集合是当前数据库状态的真实映射。整套走完一个新模块接入权限体系的工作就收口了。之前有次上线前我给测试账号直接开了超管角色权限链路完全没验结果正式环境里业务用户也能看到内部按钮排查半天才发现是角色标记了is_super。从那以后我每次加模块都强制走一遍“普通账号登录 → 接口 403 → 绑定角色 → 重新登录 → 接口 200”的路径顺手把权限码复制到菜单表和按钮指令里。先验证链路再谈功能开发能省掉后面大量返工时间。这个习惯坚持下来权限相关的线上事故几乎绝迹。希望帮到你。本文还有配套的精品资源点击获取
返回列表