尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
基于 .Net9、Vue3 与 uniapp 的多端权限管理系统全栈实现
简介这是一套基于 .Net9 与 Vue3 Element Plus、uniapp uviewui 构建的前后端分离权限管理系统适合需要快速搭建企业级后台管理框架的中高级 .NET/前端开发者。系统覆盖多租户、数据权限、动态 Api、任务调度、OSS 文件上传、滑块拼图验证、主流数据库自由切换、统一认证授权、分布式缓存、分布式事务、全 Api 鉴权、健康检查与接口文档等常见企业需求可直接作为中台管理项目底座进行学习或二次开发。压缩包共 1409 个文件大小 15.24MB以 594 个 C# 源码、273 个 Vue 组件、116 个 JSON、103 个 TypeScript 等为主辅以 37 个 SCSS 样式、16 个 csproj 工程文件、14 个 ejs 模板与 8 个解决方案文件能清晰呈现后端基础设施、前端页面模块与工程化配置。压缩包内还附带安装、构建与运行脚本及 nlog 日志配置便于快速启动调试和替换组件。目前已有 85 人学习下载适合希望从代码层面理解权限体系、多租户与分布式常用组件落地方式的开发者参考。1. 从「权限系统」到「多端权限基座」这套 Net9 Vue3 uniapp 方案到底解决了什么做过的后台管理系统多了你会发现权限模块的代码重复得最狠也最容易被翻新项目重新写一遍。某次给一个同时需要 PC 管理端和移动办公端的项目做技术选型时我被一句话打动能不能前后端分离、一套权限逻辑同时覆盖 Web 和 App这就是我后来拆解这套基于 .Net9 与 Vue3 Element Plus、uniapp uviewui 的前后端分离权限管理系统的起因。它不是普通的增删改查脚手架而是把多租户、数据权限、动态 API、任务调度、OSS 文件上传、动态高级查询这些几乎每个后台项目都要面对的模块全打通的一套权限开发底座。适合正在选型后台管理框架的团队也适合想通过一个完整主线快速上手 .NET 9 权限开发的从业者。2. 后端骨架动态 API、多租户与数据权限的三个落点2.1 动态 API 是怎么「长」出来的反射注册与控制器工厂第一次跑起这套系统时我对着后端工程找了一大圈 Controller结果发现 Controllers 目录基本是空的。后来顺着启动逻辑往下看才明白动态 API 的核心思路不手动写控制器而是用一个统一的处理入口接管特定前缀的请求再通过反射把业务 Service 映射成可调用的接口。常见做法是让业务 Service 继承一个空接口标记比如IDynamicApi启动时用ApplicationPartManager把它注册进 MVC 管道。核心逻辑大致是这样// 程序集扫描找出所有实现了 IDynamicApi 标记的 Service var dynamicApiTypes assembly.GetTypes() .Where(t t.IsClass !t.IsAbstract typeof(IDynamicApi).IsAssignableFrom(t)); foreach (var type in dynamicApiTypes) { // 以 CamelCase 类名作为路由段如 UserService - /api/user var routePrefix type.Name.Replace(Service, ).ToCamelCase(); builder.Services.AddScoped(type); // 动态控制器由后续的 DynamicApiControllerFeatureProvider 提供 }这段代码的思路是把每个业务 Service 自动映射成api/{serviceName}/{action}形式的接口省掉每个模块单独写 Controller 的重复劳动。实现层常用DynamicApiControllerFeatureProvider在ConfigureApplication时把 Service 的方法包装成 Action。需要留意的参数有两个一是路由前缀的全局限定我一般要求所有动态接口统一带api前缀避免和静态 Controller 冲突二是方法同名重载的处理动态 API 无法自动区分重载建议服务层不要让公开方法同名否则接管的入口会直接报错。这套机制上手很快但它也有明显的边界。动态 API 自动把实体的属性暴露给前端如果你直接把数据库实体当作入参和出参密码哈希、内部状态字段都会被序列化出去。所以我在项目里强制约定动态 API 的入参出参必须是 DTO实体只停留在仓储层。这个约束越早定后面避坑越轻松。2.2 多租户方案共享库加租户 ID还是独立库各跑各的多租户是整个系统里最容易被低估的一块。这套系统支持两种租户模式核心差别在数据的物理隔离级别上。一个方案是每个租户一个独立数据库隔离性最强但连接字符串要动态切换数据库实例的维护成本随租户数量线性上涨。另一个方案是共享库 租户 ID所有租户的数据在同一套表里靠TenantId字段区分。这套系统默认走的是共享库方案因为对大多数中小后台项目来说租户数量在几十个以内共享库的 TCO 明显更低。摘一段我在项目里常用的租户上下文解析逻辑// 从请求头解析租户标识存入 AsyncLocal 而不是静态变量 public class TenantContext { private static readonly AsyncLocalstring _tenantId new(); public static string CurrentTenantId { get _tenantId.Value; set _tenantId.Value value; } } // 在中间件中为每个请求设置独立的租户上下文 public async Task InvokeAsync(HttpContext context) { var tenantId context.Request.Headers[X-Tenant-Id].FirstOrDefault(); if (string.IsNullOrEmpty(tenantId)) { context.Response.StatusCode 400; await context.Response.WriteAsync(缺少租户标识); return; } TenantContext.CurrentTenantId tenantId; await _next(context); }这段代码的关键点在于使用AsyncLocal而不是静态变量静态变量是进程级的两个租户的请求并发进来时会互相串数据这是多租户系统最典型的翻车现场。租户 ID 放进请求头由前端统一携带后端在中间件里解析后存入上下文。表结构设计上所有业务表必须包含TenantId字段并按(TenantId, Id)建立复合索引。查询时可以通过SaveChanges拦截器自动填充也可以靠查询过滤器统一强制带上租户条件我最常用的是后者因为它连手写 LINQ 的漏网之鱼也能兜住。共享库方案还要注意一个坑跨租户的数据迁移和备份。因为所有数据在一个库里某天要单独导出一个租户的数据你就得写按TenantId过滤的导出脚本。建议在上线前就准备一套这样的脚本不要等客户提需求才临时写。2.3 数据权限过滤策略表达式树组装与规则缓存数据权限和多租户是两个维度。多租户是租户级隔离数据权限是同一租户内不同角色能看的数据范围不同。这套系统的数据权限支持几种常见策略仅本人数据、本部门数据、本部门及下级部门数据、全部数据。后端的实现思路是根据当前用户角色查出数据范围策略再动态构造一个ExpressionFuncT, bool拼到查询上。构造表达式树时要注意参数重用问题// 动态构造数据权限过滤条件 public static ExpressionFuncT, bool BuildDataScopeT(DataScopeType scopeType, long userId, long deptId, Listlong childDeptIds) { var parameter Expression.Parameter(typeof(T), x); Expression condition scopeType switch { DataScopeType.Self Expression.Equal( Expression.Property(parameter, CreateUserId), Expression.Constant(userId)), DataScopeType.Dept Expression.Equal( Expression.Property(parameter, DeptId), Expression.Constant(deptId)), DataScopeType.DeptAndChild Expression.Call( Expression.Constant(childDeptIds), typeof(Listlong).GetMethod(Contains, new[] { typeof(long) })!, Expression.Property(parameter, DeptId)), _ Expression.Constant(true) }; return Expression.LambdaFuncT, bool(condition, parameter); }这里有三个容易踩的地方。第一个是Expression.Constant(userId)和参数x的关系每次构造新表达式树时必须重新Expression.Parameter复用一个 parameter 实例在多个表达式树里会直接编译崩溃。第二个是Expression.Constant(true)这种恒真条件不能被 ORM 优化掉我一般在外层判断如果策略是「全部数据」就不拼条件。第三个是表达式树不能直接缓存后复用因为parameter引用是表达式树的一部分缓存一棵树给所有用户用查出来的是第一个用户的过滤结果。我通常只缓存「规则元数据」每次请求重新构造表达式树虽然有小幅 CPU 开销但能彻底避开这个隐患。数据权限的动态拼接在系统里是集中管理的所有业务查询都走一个DataScopeFilter组件不允许在 Controller 或 Service 里手动写条件。这是权限系统能长期保持不腐化的关键。3. 前端两套皮肤Vue3 管理端与 uniapp 移动端如何共用同一权限模型3.1 管理端动态菜单与路由守卫刷新页面后菜单不丢的写法Vue3 管理端用的是 Element Plus 那套组件体系。权限模型在前端落地时分成两层菜单权限和按钮权限。菜单权限决定你能看到哪些页面按钮权限决定页面里哪些操作按钮可用。登录成功后前端请求后端返回当前用户的菜单树和权限标识集合然后把菜单树写入本地状态动态生成路由并注册到router实例上。这里有个老生常谈的坑router.addRoute是异步的刷新页面时如果直接router.beforeEach里判断「没有路由就跳登录」会出现路由已注册但组件还没加载完的白屏问题。下面这段是我的固定写法router.beforeEach(async (to, _from, next) { const token localStorage.getItem(token) if (!token) { next(/login) return } // 首次进入或刷新后动态注册路由 if (!authStore.menusLoaded) { const menus await authStore.fetchMenus() const routes transformMenusToRoutes(menus) routes.forEach(r router.addRoute(r)) // 关键replace 而不是 next让新路由表立即生效 next({ ...to, replace: true }) return } next() })这段逻辑的要点是next({ ...to, replace: true })。如果刷新后还是next()Vue Router 会用旧路由表解析当前地址结果就是明明路由已添加却匹配不到组件。用replace强制重新导航一次让新增路由生效这是我处理同类系统时总结出的血泪经验。按钮权限我用自定义指令v-permission实现指令内部比对当前用户的权限码数组不含权限码就移除 DOM。要注意的是v-permission只能管按钮显隐真正的权限校验必须由后端接口兜底。前端权限只是体验优化后端不做校验等于没做。3.2 uniapp 移动端没有 Vue Router 时的权限处理差异移动端用 uniapp uviewui 开发和管理端最大的差异是没有 Vue Router 的概念。页面路由由pages.json决定页面跳转靠uni.navigateTo所以动态路由那一套在这里根本用不上。实际做法是登录后拉取用户信息和权限标识集合存到本地 storage 里。页面跳转时在目标页面的onLoad里做权限判断没有权限就用uni.redirectTo跳回首页同时用 uni 的showToast给用户一个无权限提示。按钮权限同样没法用 Vue 的自定义指令我封装了一个AuthWrapper组件内部用v-if判断权限集合。// 移动端请求拦截器自动携带 token 和租户 ID uni.request({ url: /api/user/info, header: { Authorization: Bearer uni.getStorageSync(token), X-Tenant-Id: uni.getStorageSync(tenantId) } })前后端分离的项目最怕 Web 端和移动端各写一套请求封装接口返回结构还不一致。我在这套系统里约定了一个统一的响应包装{ code, message, data }两端的拦截器都只解析这层结构code非 0 时统一弹错误提示code为 401 时统一跳登录。uniapp 端的uni.request不支持拦截器我就在封装层手动处理每次都检查code这也是一开始就要定好的规矩。3.3 动态高级查询让查询条件变成可配置的 JSON很多后台框架的查询条件都是写在页面里的死代码加一个筛选条件就要改前端加后端。这套系统把「动态高级查询」做成了一套通用机制前端把查询条件组织成 JSON 数组后端统一解析并转成Expression拼到底层查询上。前端搜索区是一个配置化组件每一条搜索项描述这样定义// 搜索项配置示例 const searchFields [ { prop: userName, label: 用户名, type: input, placeholder: 请输入用户名 }, { prop: status, label: 状态, type: select, options: [{ label: 启用, value: 1 }, { label: 禁用, value: 0 }] }, { prop: createTime, label: 创建时间, type: dateRange } ]后端收到表单提交的searchParams后按字段名和操作符重建查询表达式。这里有个安全红线字段名不能直接拿字符串拼到表达式里必须和实体的属性白名单比对不在白名单里的字段一律忽略。排序字段同样要白名单校验否则orderBy注入会成为一个很大的安全洞。这套机制让新模块的查询页开发变得非常快复制一份配置 JSON字段换一换就完成了。但它的代价是需要一定的前期设计投入字段类型、操作符映射、白名单配置都要提前规划好。如果只是做一两个小模块手动写查询反而更快所以我的建议是「系统里有超过五个查询页面时再全面铺开」。4. 避坑指南权限框架落地生产中最高频的六个坑4.1 动态 API 把实体的敏感字段整个暴露了出去现象接口返回数据里出现了密码哈希和内部状态字段前端调试工具一看全裸奔。原因动态 API 通过反射自动映射 Service 时默认把实体对象直接序列化返回了没有经过 DTO 裁剪。解决强制规定所有动态 API 的出入参必须是 DTO实体只能存在于仓储与领域层。在实体上标记[JsonIgnore]也可以兜底但治标不治本。我给这套系统加了一个约定测试扫描所有动态 API 的返回类型发现实体类型直接让单测失败。4.2 多租户数据串号并发请求互相读到对方数据现象用户 A 的请求偶尔能查到租户 B 的数据发生率不高但一出现就是事故。原因租户上下文存在静态字段里多个请求并发时后到的请求覆盖了先到请求的租户 ID。解决把租户 ID 的存取改成AsyncLocal或者在中间件里就把租户上下文塞进HttpContext.Items。注意AsyncLocal只保证异步流内的隔离跨线程的Task.Run还是要手动传递。上线后用两个租户账号并发压测批量接口是验证这个问题是否根治的有效手段。4.3 数据权限表达式树缓存后所有用户查出来都是同一个人的数据现象管理员配置完角色数据权限后某些用户看到的数据范围不对重启后又恢复正常。原因代码把ExpressionFuncT, bool缓存到了静态字典里但这个表达式树里的userId、deptId是第一次请求时的常量后续请求直接复用了它。解决不要缓存编译后的委托或表达式树本身只缓存数据权限规则。每次查询时拿规则重新构造表达式树虽然每次多几次反射和表达式构建的开销但这是标准和稳妥的做法。对查询性能有极致要求的场景可以对相同规则相同参数组合做了一级短生命周期缓存。4.4 定时任务在集群部署时被重复执行现象多实例部署后同一任务在多个节点同时触发数据被重复处理。原因Quartz 默认的调度器是进程内的每个实例独立调度没有分布式协调机制。解决让任务调度依赖一个共享的数据库表或 Redis 锁拿到锁的节点才允许执行。Quartz 自身也支持AdoJobStore配合Clustered配置但要先建好 Quartz 官方提供的那几张表同时给各个实例配置不同的InstanceId。这个配置在部署时最容易漏。4.5 uniapp 端 Vue3 自定义指令不生效现象在 uniapp 项目里写了v-permission指令控制按钮显隐结果指令不生效按钮全显示了。原因uniapp 对 Vue3 自定义指令的使用方式存在限制部分场景下指令没法监听 DOM 移除。解决改用组件包裹方案权限判断封装在AuthWrapper组件里用v-if控制内部插槽内容渲染。虽然比指令啰嗦但兼容性更好。我在正式项目里全部换成了组件方式没有再出现真机表现不一致的问题。4.6 动态查询的排序字段拼到 SQL 里被注入了现象请求参数里改orderField字段可以拿到接口返回之外的额外数据甚至直接报错暴露 SQL 片段。原因动态查询的排序字段是前端传的字符串后端直接拼到了 LINQ 的OrderBy里。解决排序字段和查询字段一样要做白名单校验前端传的字段名必须与实体属性名匹配才放行。另外我给排序字段加了一个固定前缀约定比如userName必须传User.Name直接从入口杜绝任意字符串拼接。5. 任务调度与 OSS 文件上传容易被忽略的配置细节5.1 任务调度的持久化策略进程内调度器只适合单实例开发这套系统内置了任务调度模块界面化管理定时作业。框架默认支持进程内的RAMJobStore但如果重启服务或者多实例部署运行中的任务状态会丢失。生产环境我一般用数据库持久化配置如下{ Quartz: { SchedulerName: PermissionScheduler, StoreType: AdoJobStore, DataSource: QuartzDb, TablePrefix: QRTZ_, InstanceId: INSTANCE_AUTO, Clustered: true } }Clustered设置为true后多个实例共享同一套 Quartz 表同一任务只会被一台实例触发。TablePrefix要根据你导入的建表脚本实际前缀设置默认是QRTZ_。首次搭建时别忘了先执行官方 SQL 脚本建表否则调度器启动就报缺表的错。5.2 OSS 文件上传的直传方案签名上传与服务端回调验证文件上传如果都走后端中转大文件场景下后端带宽会先扛不住。这套系统的 OSS 模块同时支持后端代理上传和服务端直传效率优先的模块用直传。直传流程是前端请求后端获取上传凭证签名 URL 和过期时间前端直接把文件 PUT 到 OSSOSS 上传完成后向配置好的回调地址发一个通知后端收到回调后更新文件记录。这里最容易出安全问题的地方在回调通知如果不验签任何人都可以伪造回调通知伪造一条上传成功记录。// 校验 OSS 回调签名防止伪造上传通知 public bool VerifyOssCallback(string authorization, string callbackBody, string publicKey) { var decodedKey Convert.FromBase64String(authorization); // 先比较 key再从公钥获取签名并和 body 的计算结果比对 // 注意 body 必须用原生字符串参与签名计算不能用已解析的对象 var expected RsaHelper.VerifyData(publicKey, callbackBody, decodedKey); return expected; }签名校验时有一个我踩过的坑回调 body 必须用 OSS 原始推送的字符串参与验签如果先 JSON 解析再序列化字段顺序一变签名就对不上。回调验证成功后还要顺手校验文件名是否和签名时记录的一致防止上传了一个完全不同的文件。5.3 存储目录与租户隔离OSS 对象名里带上租户 IDOSS 模块还有一个容易被忽略的点对象存储的文件路径设计。多租户系统里不同租户的文件不能混在一个目录下否则后续做存储生命周期管理或租户数据导出时非常痛苦。我约定的对象名规则是uploads/{tenantId}/{yyyy}/{MM}/{guid}.{ext}。这条规则有三个好处租户数据天然隔离按前缀即可做目录级授权按年月分目录后续归档和清理冷数据更方便文件名用 GUID 避免重名覆盖。要注意的是文件扩展名要做白名单校验不然别人可以上传一个 HTML 文件然后通过存储域名直接访问造成存储型 XSS。6. 自定义 API 管道与调试技巧把动态 API 的权限校验过程打开看动态 API 屏蔽了 Controller 层的样板代码但也带来了一个副作用看不到请求的分发过程出了问题难以定位。我的做法是给动态 API 增加一个自定义约定让所有动态接口在执行前打出一条结构化日志把租户上下文、当前用户、数据权限规则都打印出来。具体实现是在DynamicApiControllerFeatureProvider之后加一个IAsyncActionFilterpublic class ApiAccessLogFilter : IAsyncActionFilter { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var tenantId TenantContext.CurrentTenantId; var userId context.HttpContext.User.FindFirst(uid)?.Value; var actionName context.ActionDescriptor.RouteValues[action]; // 打印请求摘要方便排查权限问题 Console.WriteLine($[API] tenant{tenantId} user{userId} action{actionName}); var result await next(); if (result.Exception ! null) { Console.WriteLine($[API] exception{result.Exception.Message}); } } }这个过滤器让每个动态接口的调用都有痕迹可查排查数据权限问题时不至于全靠猜。相比直接改框架源码加一层过滤器是侵入性最小的调试手段。另一个值得养成的习惯是新拉一个模块时先把它的增删改查和四条数据权限策略全部写一遍测试用例覆盖本人、本部门、部门及下级、全部数据四个范围再开始写页面。以前我为了赶进度跳过过这条流程结果上线后用户反馈数据可见范围不符合预期排查了很久才发现是某个查询忘了拼接数据权限过滤器。从那以后我每次加完模块都会强制走一遍这四类测试确认边界值正常再提交代码。这套流程不复杂但它能省掉大量线上排查时间。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

终端安全进化论:从被动查杀到主动免疫的体系重构

终端安全进化论:从被动查杀到主动免疫的体系重构

前阵子跟一个做安全运维的朋友聊天,他说了句特别实在的话:“现在终端安全最大的问题,不是杀毒软件装得不够多,而是我们根本防不住。”他所在的那家公司,某天夜里三点,一台研发笔记本中了勒索病毒&#xff0…

📅 2026/10/14 4:35:05
Android旅游系统App开发实战:从架构到答辩避坑全指南

Android旅游系统App开发实战:从架构到答辩避坑全指南

毕业设计如果抽到“基于Android的旅游系统”这个题目,恭喜你,这是一个非常经典、难度适中、且技术栈覆盖面相当完整的选题。它既有用户端的交互界面,又有地图和多媒体展示,还要做数据存储和请求,一套做下来&#xff0c…

📅 2026/10/14 4:35:05
上下文模式深度解析:状态隔离、作用域管理与并发安全实践

上下文模式深度解析:状态隔离、作用域管理与并发安全实践

1. 从"上下文模式"说起:一个被低估的架构决策第一次看到"context-mode"这个词,很多人会下意识地把它归类成某个框架里的配置项,或者某个库的开关参数。但如果你真正在工程一线待过几年,就会明白:上…

📅 2026/10/14 4:35:05
MORE NEWS

更多资讯

📰

ffdec-18.5.0深度指南:SWF反编译、AS3反混淆与Flash遗产抢救

简介:FFDec 18.5.0 是一款开源、跨平台的 Flash SWF 文件反编译与分析工具,专为开发者、逆向工程师及数字遗产维护者设计,用于解析已停止支持的 Flash 内容,解决 SWF 结构理解、ActionScript 代码还原、资源提取与历史项目迁移等核…

📰

gz-sim 水下机器人控制实战:LRAUV 速度、偏航与俯仰的 PD/P 控制器解析

机器人 【免费下载链接】gz-sim Open source robotics simulator. The latest version of Gazebo. 项目地址: https://gitcode.com/gh_mirrors/gz/gz-sim 点击查看 免费下载 本文以 gz-sim(Gazebo Sim)仓库中的 examples/standalone/lrauv_c…

📰

医院病房管理系统数据库课设:核心表设计、事务控制与避坑指南

简介:这份《医院病房管理系统--数据库课设》面向计算机相关专业学生与数据库初学者,提供一套完整的课程设计参考方案,帮助解决从需求分析到系统落地的全流程问题。资源包共146个文件,约5.04MB,以Java源码与编译后的cla…

📰

3D Slicer Welcome 模块指南:启动欢迎页与自动更新机制深度解析

医疗健康图像处理数据可视化桌面应用3D渲染 【免费下载链接】Slicer Multi-platform, free open source software for visualization and image computing. 项目地址: https://gitcode.com/gh_mirrors/sl/Slicer 点击查看 免费下载 导读 Welcome 模块是 3D Slicer…

📰

OpenCV2.1 C++Builder封装:老项目图像处理接入与避坑指南

简介:这份资源是面向CBuilder开发者准备的OpenCV 2.1集成示例,作者将OpenCV自带的经典示例改写为VCL版本,方便在CB环境中直接调用图像处理能力。包内包含OpenCV 2.1 for CB所需的lib库文件与include头文件,并附有使用说明&#xf…

📰

数据库课设实战:酒店客房管理系统设计与实现

简介:针对数据库课程设计中的酒店管理场景,这套以客房管理为核心的实践资源,适合高校计算机相关专业学生作为课程设计或期末项目参考。项目围绕客房、客户、订单、入住记录与退房记录等核心实体,给出了关系型数据库的表结构设计&a…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬