Blazor 数据权限怎么实现?为什么查询、新增、修改、删除都要控制?

发布时间:2026/10/8 8:18:18

Blazor 数据权限怎么实现?为什么查询、新增、修改、删除都要控制? 已发布的《企业后台权限设计数据权限和角色权限的区别有多大》讲的是概念。这篇讲实现ApplyDataPermission到底生成了什么条件为什么只做查询过滤是不够的以及导入 Excel 时那条最容易被忽略的越权路径是怎么被堵上的。一、先分清两个问题功能权限回答的是你能不能打开这个页面、能不能点这个按钮。数据权限回答的是打开页面之后你能看到、能修改哪些行。这两件事在实现上完全不同功能权限的载体是SysMenu菜单 按钮判定方法是AuthPath/AuthButton数据权限的载体是SysRole.DataPermission和实体上的OrgId判定方法是生成 SQL 过滤条件。一个很典型的漏洞场景用户确实有订单管理的编辑按钮权限功能权限没问题但他把请求里的Id改成别人的订单如果只校验按钮权限这条别人的数据就被改了。所以数据权限必须覆盖查询、新增、修改、删除、批量导入所有落库路径。二、模型一个接口 一个枚举1. 实体侧IDataPermissionpublicinterfaceIDataPermission{/// summary获取或设置组织 ID/summarylongOrgId{get;set;}}就这么简单。实体实现了它就表示这张表的每一行归属于某个组织。实际生效还需要第二个条件实体同时实现IEntityCreated提供CreatedUserId用于仅本人数据规则。源码里的自动判定// 实体实现 IDataPermission IEntityCreated 时自动启用数据权限_autoDataPermissiontypeof(IDataPermission).IsAssignableFrom(typeof(TItem))typeof(IEntityCreated).IsAssignableFrom(typeof(TItem));SysUser就是这样一个例子public partial class SysUser : EntityFull, IDataPermission而EntityFull的继承链上带了IEntityCreated。2. 角色侧五种数据范围publicenumDataPermissionType{[Display(Name全部数据)]AllData,[Display(Name本部门及以下数据)]CurrentDepartmentAndBelow,[Display(Name本部门数据)]CurrentDepartmentOnly,[Display(Name仅本人数据)]PersonalOnly,[Display(Name自定义数据)]Custom}角色上还配了CustomDataPermission逗号分隔的组织 ID 列表。数据权限是按角色配的一个用户有多个角色时取并集——这点在源码里体现得很直接。三、查询路径ApplyDataPermission1. 完整实现publicstaticISelectTApplyDataPermissionT(thisISelectTselect,AdminContextadminContext,boolenablefalse,stringuserIdFieldCreatedUserId)whereT:class{if(!enable||!typeof(IDataPermission).IsAssignableFrom(typeof(T))||!typeof(IEntityCreated).IsAssignableFrom(typeof(T))){returnselect;}varuseradminContext.User;varrolesadminContext.Roles;if(usernull||rolesnull||roles.Count0){returnselect.Where(afalse);}// 管理员角色拥有全部数据权限if(roles.Any(rr.DataPermissionDataPermissionType.AllData))returnselect;varparamExpression.Parameter(typeof(T),a);varorgIdPropExpression.Property(param,OrgId);varuserIdPropExpression.Property(param,userIdField);varuserIdConstantExpression.Constant(user.Id);Expression?combinedConditionnull;foreach(varroleinroles){Expression?roleConditionnull;switch(role.DataPermission){caseDataPermissionType.PersonalOnly:roleConditionExpression.Equal(userIdProp,userIdConstant);break;caseDataPermissionType.CurrentDepartmentOnly:caseDataPermissionType.CurrentDepartmentAndBelow:varorgIdsadminContext.GetOrgIds(role.DataPermissionDataPermissionType.CurrentDepartmentAndBelow);roleConditionBuildOrgInExpression(orgIds,orgIdProp);break;caseDataPermissionType.Custom:// 未配置或配置了非法组织 ID 时按无可见数据处理fail-closedvarcustomOrgIds(role.CustomDataPermission??string.Empty).Split(,,StringSplitOptions.RemoveEmptyEntries|StringSplitOptions.TrimEntries).Select(slong.TryParse(s,outvarid)?id:(long?)null).Where(idid.HasValue).Select(idid!.Value).ToList();roleConditionBuildOrgInExpression(customOrgIds,orgIdProp);break;}if(roleCondition!null){combinedConditioncombinedConditionnull?roleCondition:Expression.OrElse(combinedCondition,roleCondition);}}if(combinedCondition!null){varlambdaExpression.LambdaFuncT,bool(combinedCondition,param);returnselect.Where(lambda);}returnselect;}2. 逐段拆解1不满足条件就不过滤enable false、实体没实现IDataPermission、或者没实现IEntityCreated都直接返回原查询。这是显式开关 自动识别的双保险不实现接口的实体不会被误伤。2拿不到用户或角色时 fail-closedif(usernull||rolesnull||roles.Count0){returnselect.Where(afalse);}注意这里是Where(a false)不是返回全部。权限上下文不完整时宁可看不到数据也不能放开。3AllData 直接短路只要用户有任意一个AllData角色直接返回原查询不生成任何条件。管理员角色IsAdministrator在角色页里通常也配成全部数据。4多角色取并集每个角色生成一个条件表达式最后用Expression.OrElse串起来。举例用户有A 角色本部门和B 角色仅本人两个角色最终条件等价于(OrgIdIN(本部门及子部门)ORCreatedUserId当前用户)这符合角色叠加只增不减的直觉。5本部门用树形 CTE 算范围publicListlongGetOrgIds(boolincludeChildren){if(User?.OrgIdnull)return[];varorgIdsnewListlong();if(includeChildren){varchildOrgsOrm.SelectSysOrg().Where(aa.IdUser.OrgId).AsTreeCte().ToList();orgIds.AddRange(childOrgs.Select(aa.Id));}else{orgIds.Add(User.OrgId);}returnorgIds;}AsTreeCte()让数据库递归展开整棵子树。“本部门及以下就是自己 所有子孙部门”本部门只取一个 ID。6自定义非法配置按无数据CustomDataPermission里解析不出任何合法 ID 时BuildOrgInExpression会返回privatestaticExpressionBuildOrgInExpression(ListlongorgIds,MemberExpressionorgIdProp){if(orgIds.Count0)returnExpression.Constant(false);...}也就是WHERE 1 0。这条 fail-closed 设计很重要——配置漏了不该等于看到全部数据。3. 生成的 SQL 长什么样以本部门及以下为例最终落到数据库上大致是SELECT*FROMblog_articleWHEREOrgId100OROrgId101OROrgId105;仅本人则是SELECT*FROMblog_articleWHERECreatedUserId9001;条件是表达式树直接拼进查询的不是取出数据后在内存里过滤。这意味着分页Count、导出、排序都作用在同一个过滤后的集合上不会出现第一页看着正常第二页混进别人的数据。四、新增路径OrgId 由框架自动写查询过滤做好了新增时如果OrgId是手填的或者默认 0数据就会掉进谁都看不到或者被误认为属于 0 号组织的坑。框架在AdminExtensions.cs里通过RepositoryOptions.AuditValue统一处理AuditValuee{varuserr.GetServiceAdminContext()?.User;if(usernull)return;// 插入操作时设置创建用户信息if(e.AuditValueTypeAuditValueType.Inserte.ObjectisIEntityCreatedobj1obj1!null){obj1.CreatedUserIduser.Id;obj1.CreatedUserNameuser.Username;obj1.CreatedTimeDateTime.Now;}// 更新操作时设置修改用户信息if(e.AuditValueTypeAuditValueType.Updatee.ObjectisIEntityModifiedobj2obj2!null){obj2.ModifiedUserIduser.Id;obj2.ModifiedUserNameuser.Username;obj2.ModifiedTimeDateTime.Now;}// 用户部门if(e.AuditValueTypeAuditValueType.Inserte.ObjectisIDataPermissionobj3obj3!null){obj3.OrgIduser.OrgId;return;}}所以业务代码里不需要也不应该手填OrgId插入时它会被当前用户的组织覆盖。五、修改 / 删除 / 批量操作FilterAuthorizedAsync查询用 SQL 过滤没问题但修改和删除是按主键直接落库的没有先查再改这个过程。如果只靠前端传过来的实体伪造Id就能越权。所以框架提供了第二个入口/// summary/// 从候选记录中筛出当前用户确实有权限操作的记录仅依据主键回查条件与查询数据权限一致。/// 用于 Excel 导入 / 批量更新 / 批量删除等按主键直接落库的路径/// 防止只靠前端按钮权限而被伪造 Id 绕过。/// /summarypublicstaticasyncTaskListTFilterAuthorizedAsyncT,TKey(thisIAggregateRootRepositoryTrepo,AdminContextadminContext,IEnumerableTcandidates,boolenablePermission,stringuserIdFieldCreatedUserId)whereT:class,IEntityTKey,new(){varlistcandidates?.ToList()??[];if(list.Count0)return[];if(!repo.Select.IsDataPermissionEnabled(adminContext,enablePermission)){returnlist;}varidslist.Select(xx.Id).Distinct().ToList();varauthorizedawaitrepo.Select.Where(aids.Contains(a.Id)).ApplyDataPermission(adminContext,true,userIdField).ToListAsync(aa.Id);varauthorizedSetnewHashSetTKey(authorized);returnlist.Where(xauthorizedSet.Contains(x.Id)).ToList();}它的做法是按主键回查一次把候选 Id 丢回数据库用和查询完全相同的ApplyDataPermission条件筛一遍只保留查得到的主键。查不到的说明越权。用回查而不是在内存里比对 OrgId是有意为之规则只有一份查询和写操作不会不一致本部门及子部门的展开是数据库算的内存里没有完整组织树自定义数据权限的解析逻辑也复用同一段代码。AdminTable在更新、删除、软删除路径上都调用了它// 更新if(changedTypeItemChangedType.Update){varauthorizedawaitFilterAuthorizedAsync([item],CommonLocalizer[没有权限修改该数据]);if(authorized.Count0)returnfalse;}// 删除itemsawaitFilterAuthorizedAsync(items,CommonLocalizer[没有权限操作部分数据已取消删除]);if(items.Count0)returnfalse;六、Excel 导入最容易被忽略的越权入口导入的默认实现是InsertOrUpdateId 0的行按主键更新。也就是说Excel 里写什么主键就可能更新哪一行。如果这里只做按钮权限校验能不能用导入是过了但能导谁的数据完全没管。所以AdminTable在导入路径上单独加了过滤privateasyncTaskListTItemFilterAuthorizedImportRowsAsync(ListTItemrows){if(rows.Count0||!(UseDataPermission||_autoDataPermission)){returnrows;}varauthorizedawaitFilterAuthorizedAsync(rows);if(authorized.Countrows.Count){varrejectedrows.Count-authorized.Count;awaitToastService.Warning(CommonLocalizer[导入结果],string.Format(CommonLocalizer[已忽略 {0} 条没有权限操作的数据。],rejected));}returnauthorized;}调用点在导入主流程里// 数据权限Excel 导入/更新同样不能操作当前用户无权限的数据。// Id 0 的行会走数据库按主键更新因此必须逐行确认该记录在当前用户的数据权限范围内// 不能只依赖前端按钮权限否则伪造 Id 即可越权更新他人数据rowsawaitFilterAuthorizedImportRowsAsync(rows);if(rows.Count0)return;affectedRowsawait_repo.Orm.InsertOrUpdateTItem().SetSource(rows).UpdateColumns(updateColumns).ExecuteAffrowsAsync();行为是剔除 提示而不是整批失败有权限的行正常导入越权的行被丢弃用户能看到已忽略 N 条没有权限操作的数据。如果你用OnImportAsync完全接管导入记得自己补这一步——默认过滤不会执行。七、测试是怎么锁定这些行为的EasyAdminBlazor.Tests/Security/DataPermissionTests.cs用一套真实 SQLite 数据库覆盖了四条路径每条都是必须有权限的留下、越权的剔除测试覆盖路径断言Query_OnlyReturnsAuthorizedRows查询结果只包含本组织OrgId 100Query_CannotReadOtherUsersData查询结果中不出现OrgId 200Update_ForgedIdOutsideScope_IsRejected更新伪造 Id 被拦截数据库状态未变Update_WithinScope_IsAllowed更新权限范围内可正常更新ExcelImport_OtherOrgRow_IsDropped导入混入的他人行被剔除只保留自己的Delete_OtherOrgRows_AreFilteredOut删除越权记录未被删除Admin_RoleWithAllData_SeesEverything管理员AllData角色不受限DisabledDataPermission_DoesNotFilter关闭开关保持既有行为不做过滤这些用例的价值不只是测试过了而是把设计意图写成了可执行的约束四条路径都不能只靠 UI 权限。八、边界与坑userIdField默认是CreatedUserId。仅本人规则按这个字段判定。如果你的业务里归属人不是创建人要在调用时传入正确的字段名。实体必须同时实现IDataPermission和IEntityCreated否则自动启用不生效显式传UseDataPermissiontrue也不会过滤ApplyDataPermission里有接口判断。AdminTable的自动启用只发生在实体实现两个接口时这时不需要页面写UseDataPermission。AdminSelectTable/AdminMultiSelect也支持数据权限用法一致UseDataPermission 同样的接口约定。自定义OnBeforeQuery里做的 Join / Include 要留意数据权限条件作用在TItem上Join 出来的关联表不会自动带过滤条件。文件等非标准资源要单独判定。SysFileController就没有复用IDataPermission而是明确注释了文件实体本身不实现IDataPermission因此采用与数据权限一致的判定并且上传人缺失时拒绝访问。它不是通用行级权限引擎。按金额区间、按状态、按自定义表达式这类复杂规则不在当前模型里需要自己在OnBeforeQuery/ 写路径上补充。九、小结把数据权限的实现串起来就是四个位置、一个原则路径实现查询ApplyDataPermission生成 SQL 过滤条件新增AuditValue自动写入当前用户的OrgId修改 / 删除 / 批量FilterAuthorizedAsync按主键回查权限范围Excel 导入FilterAuthorizedImportRowsAsync逐行过滤 提示一个原则权限上下文不完整时 fail-closed。拿不到用户、拿不到角色、自定义配置解析为空一律看不到任何数据而不是放开全部。功能权限决定你能进哪扇门数据权限决定你在门里能碰哪些东西。两者都做才叫权限控制。如果你正在用 .NET 10 Blazor 做企业后台数据隔离通常是绕不开的需求。EasyAdminBlazor 的数据权限模型可以直接用需要扩展时也能顺着FreeSqlExtensions改。文档https://easyadmin.wang-zhan.com.cn/doc源码https://gitee.com/gudufy/EasyAdminBlazor
延伸阅读

更多相关文章

2026/10/8 8:18:18

Blue---贪心

前言 贪心算法的学习重点我想放在见多识广上,轻证明甚至不证明,如果一道题你没有任何思路,不妨想想贪心能不能做,当然,其实我们能想到的贪心策略取决于我们做过多少道题。一般就是题目里说求什么最少,什么最…

2026/10/8 9:03:30

保姆级教程:Windows下MySQL 9.1.0安装全流程解析

MySQL 9.1.0 发布之后,这段时间经常有人来问我同一个问题:不是问它跟 8.4 LTS 到底差多少,而是问“怎么装”。也确实,MySQL 官网的下载页对新手来说就是一本天书,一堆版本号横七竖八地排在那里,下面还有 ZI…

2026/10/8 9:03:30

HDMI2.1与eDP TX接口设计实战:从眼图测试到信号完整性排查

一块板子拿到手,第一次插上显示器就花屏或者直接黑屏,这种场景做硬件的人应该都不陌生。HDMI2.1、eDP这类高速视频TX接口,说难其实不算难,但坑的位置非常固定:高速差分信号怎么走、AC耦合电容放哪边、阻抗控制到多少、…

2026/10/8 9:03:30

双碳大模型实战:碳核算报告生成与CCUS比选

简介:一份聚焦大模型技术在碳排放与碳回收(双碳)领域应用的系统方案,内容从全球碳排放现状背景讲起,梳理工业化、能源消耗、交通、农业等主要驱动因素,并详细介绍化学吸收法、膜分离法、生物固定法、物理吸…

2026/10/8 9:03:30

OpenClaw(龙虾)部署实战:从Windows、安卓到腾讯云免费算力

说实话,我一开始看到“龙虾”OpenClaw全国巡装、腾讯云免费装机这种消息,第一反应是:这又是什么圈子里的新梗?结果顺着关键词一查,才发现这压根不是玩梗,而是一个正在快速升温的AI个人助理开发项目在往线下…

2026/10/8 9:03:30

Canal启动报错:Could not find first log file name 根因排查与解决

最近在帮团队搭建数据同步管道,启动Canal时报了一个看起来挺唬人的错误:Could not find first log file name in binary log index file。这个错误估计不少用过Canal的朋友都撞上过,第一次看到的时候我还愣了一下,毕竟Canal已经配…

2026/10/8 8:58:29

蠕虫病毒传播链与分层防御:从应急响应到内网加固实战指南

周五晚上十点,我正在家看球赛,手机突然连震三次。值班同事在群里发消息:核心交换机流量异常,内网大量主机互相发包,OA系统已经打不开了。紧接着远程连服务器,ssh敲下去卡了十几秒才出提示符,upt…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/7 8:18:33

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 0:02:17

自然数立方等于连续奇数之和:从证明到编程验证

十几年来我一直游走在数学科普和编程教学这两块内容之间,对“看起来像魔法、拆开全是数学”的结论总是格外敏感。最近翻资料时又撞见一句话:任何一个自然数 m 的立方,都可以写成 m 个连续奇数之和。2 的立方等于 3 加 5,3 的立方等…

2026/10/8 0:02:17

C#上位机SSH连接实战:用SSH.NET补齐超时、批量与密钥认证

简介:这是一份基于 C# 开发的 SSH 连接功能半成品工程,原本作为另一个主项目的子功能模块,现独立打包分享。工程采用 WinForms 界面,包含源码、解决方案、安装部署工程、NuGet 依赖包及说明文档,适合正在做远程连接、网…

2026/10/8 0:02:17

Java SpringBoot一体化智能售后系统设计与实现全解析

毕业设计年年做,Java Web 方向的题目翻来覆去就那么几个,但“一体化智能售后系统”这个题,每次看到我都觉得值得认真聊一聊。它不是一个简单 curd 堆出来的管理系统,而是把客户、工单、派单、处理、回访、统计整条链路串起来的一套…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑