岱山县建设网站,想学做网站报班,网站接入空间,西安区seo搜索排名优化前言
之前笔者写过一篇推广Blazor的博客《安利一下Blazor#xff1a;.NET开发者的全栈“优”选项》#xff0c;简单的聊过一点Blazor的话题#xff0c;以及它和一些前端框架#xff08;如Vue#xff0c;React#xff09;的异曲同工之处。
近期在开发的一个基于Blazor S…前言之前笔者写过一篇推广Blazor的博客《安利一下Blazor.NET开发者的全栈“优”选项》简单的聊过一点Blazor的话题以及它和一些前端框架如VueReact的异曲同工之处。近期在开发的一个基于Blazor Server框架的管理后台项目由于对管理人员权限的要求比较细致这里我们根据实际的场景需求我想分享一下我在项目中通过组件化封装实现的一套覆盖维度比较广的权限方案并探讨在架构设计中如何正确看待系统的“复杂度”。本文提到的“Blazor”统一代表“Blazor Server”模式而Blazor的另一个架构模式Blazor WeebAssembly不能代入本文。场景在我的场景里对权限的控制需求基本是这样的有上帝视角的超级管理员可以对系统进行一切操作但操作都有记录超级管理员可以创建不同的角色并分配权限权限类型分为路由访问权限有权限就可以进入这个界面没有就不能活动管理权限即数据权限活动数据是系统管理的中心模型权限可以绑定不同的活动然后具备该权限的管理员才可以看到对应的活动活动子权限意思是活动下面还有附加的属性比如组别区域等这些也可以单独授权比如A管理员拥有活动1下A区域和A组别的权限B拥有活动1下B区域和B组别的权限以此类推这属于细化的数据权限还有一种组件使用权限我们开发的一些功能组件也要纳入权限控制范围具备该权限的用户才能使用这些组件比如数据导出附加下载等为了支撑业务需求我将权限模块拆解为四个逻辑层级上帝视角超级管理员拥有全局最高权限绕过所有过滤逻辑但所有操作由拦截器记录审计日志。页面路由权限控制用户能否进入特定功能模块。组件功能权限控制页面内具体操作如导出、下载的可见性与可用性。精细化数据权限横向隔离常规管理员仅能看到授权给他的“活动”数据。纵向细分在特定活动内进一步根据“赛区”和“组别”进行过滤。技术实现实际上在稍微上点规模的管理系统里权限模块的设计都是直接体现其系统质量好坏的一个典型标准。首先你不可能想小项目一样到处写判定写魔法值而是应该将分布式的权限控制进行收敛将复杂的问题归一化既保证权限的精准控制又保证操作的灵活性和便利性。我这里是通过两个核心组件将权限校验从繁杂的业务代码中剥离出来。1. 权限守卫组件PermissionGuard这个组件负责“拦截”。它支持权限码Key和角色Role的双重校验并且处理了异步加载时的占位状态。PermissionGuardPermissionKeypermission.editAuthorizedMudButtonVariantVariant.FilledColorColor.Primary编辑/MudButton/AuthorizedNotAuthorizedMudButtonDisabledtrue无权访问/MudButton/NotAuthorized/PermissionGuard这里主要有两点值得提一下状态处理内置了dataLoaded 状态在权限异步计算完成前显示一个加载动画避免 UI 闪烁就是每次加载页面前闪现一下“无权访问”。参数响应重载了生命周期函数OnParametersSetAsync当传入的权限 Key 发生变化时能够自动重新计算权限状态。同时对权限数据进行了缓存提高性能。注意PermissionGuard的拦截操作并不是隐藏元素而是在服务器渲染阶段就决定了组件树的构成。如果权限校验不通过相应的Html标签和事件代码等根本不会被发送到客户端。组件的csharp代码部分我稍微灌一点主要还是思路的分享。[Parameter]publicstringPermissionKey{get;set;}string.Empty;[Parameter]publicstring[]PermissionKeys{get;set;}Array.Emptystring();// 省略传递的入参这里还可以传递角色名称等privatebooldataLoadedfalse;// 部分参数定义省略protectedoverrideasyncTaskOnInitializedAsync(){awaitLoadPermissionAsync();}// 权限的计算逻辑要落到这里Blazor的生命周期函数拿捏真的太精准了protectedoverrideasyncTaskOnParametersSetAsync(){if(ShouldRecalculatePermission()){awaitLoadPermissionAsync();}}privateboolShouldRecalculatePermission(){//是否需要重新计算权限略}//加载privateasyncTaskLoadPermissionAsync(){_cachedPermissionKeyPermissionKey;_cachedPermissionKeysPermissionKeys?.ToArray()??Array.Emptystring();_cachedRoleRole;_cachedRolesRoles?.ToArray()??Array.Emptystring();_cachedRequireAllRequireAll;_hasPermissionawaitCalculatePermissionAsync();_isFirstRenderfalse;dataLoadedtrue;}privateboolArraysEqual(string[]array1,string[]array2){// 比对逻辑}执行效果2. 数据过滤组件DataScopeFilter通过“拦截”的方式可以方便的实现路由守护组件渲染等控制再下沉到数据层面光靠“PermissionGuard”就不够了。因此我又实现了一个数据过滤的组件这是实现数据权限的核心。它不参与 UI 渲染而是作为一个切面它通过维护一个统一的筛选条件构造器 (DynamicFilterBuilder)在数据请求前自动完成 SQL 过滤条件的构建。这里我多说两句如果你对传统WebForms框架足够了解看到Blazor的组件设计应该会十分亲切Blazor 与 ASP.NET Web 窗体有很多共同之处实现效果相似但实现逻辑已经完全不同了更贴近Vue之类的现代前端框架里的组件。但对基于WebForm的系统来说Blazor仍然是最好的转型方向。https://learn.microsoft.com/zh-cn/dotnet/architecture/blazor-for-web-forms-developers/introduction核心代码逻辑组件会调用DataScopeService 获取当前用户的数据范围DataScopeInfo然后根据这些信息自动操作FilterBuilder处理场景有全局权限如果是超级管理员不添加任何过滤条件。活动过滤如果用户只负责特定活动则自动注入限定条件如where DecMainId in (…) 。细颗粒度过滤这是最复杂的部分。组件会判断是否启用了UseDecMainAuxFilter如果启用会针对特定的活动 ID嵌套加上区域Area和组别Group的过滤逻辑。在父组件里的调用案例如下DataScopeFilterFilterBuilderfilterBuilderUseDecMainAuxFiltertrueMudTableItems_data!--表格内容--/MudTable/DataScopeFiltercode{privateDynamicFilterBuilderfilterBuildernew();// 伪代码数据查询时自动应用权限过滤privateIQueryableActivityGetFilteredData(){return_repository.Query().Where(filterBuilder.Build())// 自动注入权限条件.ToList();}}上述组件的特性“FilterBuilder”就是我在项目中全局维护的一个检索式构造器父组件中提交查询时会使用这个构造器同时DataScopeFilter里会自动注入拼接好的范围进而实现数据过滤的效果。SQL构造原理简述组件内部会根据用户权限生成对应的查询条件。例如无子权限WHERE DecMainId IN (1,2,3)有子权限WHERE (DecMainId IN (1,2,3) AND DecAreaId IN (101,102)) OR (DecMainId IN (4,5))这种条件组合确保了用户只能看到自己被授权的数据且对上层业务代码完全透明。而UseDecMainAuxFilter也是系统专属的一个特性属于对更细粒度的权限控制当开启之后会对活动属性再进行一次逻辑过滤部分的逻辑代码如下usingxxx inject IDataScopeServiceDataScopeServiceif(!dataLoaded){MudPaperClasspa-16 ma-2Elevation0MudCardMudCardContentMudProgressCircularColorColor.PrimaryStyleheight:100px;width:100px;IndeterminatetrueSizeSize.LargeChildContentloading.../ChildContent/MudProgressCircular/MudCardContentMudCardActionsMudButtonVariantVariant.TextColorColor.Primary数据权限加载中.../MudButton/MudCardActions/MudCard/MudPaper}code{[Parameter]publicInfrastructures.DynamicFilterBuilder?FilterBuilder{get;set;}[Parameter]publicboolUseDecMainAuxFilter{get;set;}false;// 其他参数省略/// summary/// 自动应用数据权限过滤/// /summaryprivatevoidApplyDataScopeFilter(DataScopeInfodataScopeInfo){// 自动应用数据权限过滤的逻辑片段if(UseDecMainAuxFilterdataScopeInfo.DecMainWithAuxes.Any()){FilterBuilder.Or(group{// 针对有细化权限的活动拼接特定的区域和组别条件foreach(varauxItemindataScopeInfo.DecMainWithAuxes){group.And(subGroup{subGroup.Add(FilterFieldName,auxItem.DecMainId);subGroup.Add(FilterFieldNameAuxArea,auxItem.DecAreaIds,DynamicFilterOperator.Any);// ... 更多细化逻辑});}});}}}过滤服务为了更好的实现“过滤”我这里封装了一个底层的服务“DataScopeService”它的核心职责只有一个根据当前用户的身份和角色动态计算出他/她有权访问的活动DecMainID 列表及其附属范围如区域、分组。它是一个“决策后端”每当用户进入一个需要数据筛选的页面比如活动列表页过滤组件会调用 GetCurrentUserDataScopeAsync() 获取当前用户的权限范围。核心代码逻辑如下// 权限范围信息publicclassDataScopeInfo{publicListlongDecMainIds{get;set;}new();publicListDecMainWithAuxDecMainWithAuxes{get;set;}new();publicboolHasAllDataAccess{get;set;}true;// 默认超级管理员}publicclassDecMainWithAux{publiclongDecMainId{get;set;}publicListlong?DecAreaIds{get;set;}publicListlong?DecGroupIds{get;set;}}publicinterfaceIDataScopeService{TaskDataScopeInfoGetCurrentUserDataScopeAsync();voidClearCache();}publicclassDataScopeService:IDataScopeService{privatereadonlyAuthenticationStateProvider_authStateProvider;// ...其他依赖略privateDataScopeInfo?_cachedDataScope;privateDateTime_cacheTimeDateTime.MinValue;privatestaticreadonlyTimeSpanCacheExpirationTimeSpan.FromMinutes(5);publicasyncTaskDataScopeInfoGetCurrentUserDataScopeAsync(){// 缓存有效则直接返回if(_cachedDataScope!nullDateTime.Now-_cacheTimeCacheExpiration)return_cachedDataScope;varuser(await_authStateProvider.GetAuthenticationStateAsync()).User;if(!user.Identity?.IsAuthenticated??true)returnnewDataScopeInfo{HasAllDataAccessfalse};// 从 Claims 获取当前管理员 IDif(!long.TryParse(user.FindFirst(ClaimTypes.NameIdentifier)?.Value,outvaradminId))returnnewDataScopeInfo{HasAllDataAccessfalse};// 获取该管理员的所有角色 → 角色对应的权限 → 类型为“活动管辖权”的权限varroleIdsawaitGetAdminRoleIds(adminId);varpermissionIdsawaitGetPermissionIdsByRoles(roleIds);vardataPermissionsawaitGetEnabledDataPermissions(permissionIds);// 若无数据权限配置默认无访问权if(!dataPermissions.Any())returnnewDataScopeInfo{HasAllDataAccessfalse};vardecMainIdsnewHashSetlong();vardecMainAuxesnewHashSetDecMainWithAux();foreach(varpermindataPermissions){if(!string.IsNullOrEmpty(perm.DataFilterJson))ParseDecMainIds(perm.DataFilterJson,decMainIds);if(!string.IsNullOrEmpty(perm.DataAuxFilterJson))ParseDecMainWithAux(perm.DataAuxFilterJson,decMainAuxes);}varscopenewDataScopeInfo{DecMainIdsdecMainIds.ToList(),DecMainWithAuxesdecMainAuxes.ToList(),HasAllDataAccessfalse// 只要配置了权限就视为受限用户};_cachedDataScopescope;_cacheTimeDateTime.Now;returnscope;}privatevoidParseDecMainIds(stringjson,HashSetlongtarget){vardictJsonSerializer.DeserializeDictionarystring,JsonElement(json);if(dict?.TryGetValue(DecMainIds,outvararray)true){foreach(varidinarray.EnumerateArray())target.Add(id.GetInt64());}}privatevoidParseDecMainWithAux(stringjson,HashSetDecMainWithAuxtarget){varlistJsonSerializer.DeserializeListDecMainAuxDto(json);if(listnull)return;foreach(variteminlist){target.Add(newDecMainWithAux{DecMainIditem.DecMainId,DecAreaIdsitem.DecAreaScopes?.Select(xx.Id).ToList(),DecGroupIdsitem.DecGroupScopes?.Select(xx.Id).ToList()});}}publicvoidClearCache()_cachedDataScopenull;}这里有一个小Tips在DataScopeService中我用 HashSet 来收集用户可访问的活动 IDDecMainIds而不是 List。原因很简单自动去重多个角色可能授权了同一个活动 IDHashSet 天然避免重复省去手动判重逻辑高效查找后续做权限校验如 Contains(decMainId)时HashSet 的平均时间复杂度是 O(1)而 List 是 O(n)集合运算友好未来若需支持“交集/并集”等权限合并逻辑比如“仅查看 A 和 B 角色共管的活动”HashSet 提供 IntersectWith、UnionWith 等内置方法简洁又高效。事实上在系统其它地方我也大量使用了这种数据结构当你需要存储唯一值且频繁进行存在性判断或集合操作时HashSet 往往比 List 更合适——尤其在权限、标签、ID 列表等场景中性能又好又简洁。但要注意一个点HashSet是非线程安全的使用时确保其组件或服务注入的作用域是Scoped如下services.AddScopedIDataScopeService,DataScopeService();执行效果调整数据权限之前调整之前这里可以看“保定”区域以及“小学组”的数据调整数据权限调整时增加对“石家庄”和“中学组”的访问权限调整数据权限之后此时再回到刚才的界面统一活动条件下筛选框里就变成了“保定”“石家庄”和“小学组”“中学”了至此通过2个不同的组件将复杂的权限控制都收敛到了统一的地方实现灵活多样的权限控制。架构思考设计这套模块时我并没有选择“最快”的路我也曾犹豫是否应该简化设计只做简单的角色权限复杂的业务需求用文档或培训来解决也就是所谓的“先跑起来有时间再回来优化”实际上大家都清楚不会再有时间了。掩盖复杂性不会让它消失只会让它以Bug维护成本以及繁琐的沟通形式加倍偿还。在架构设计中复杂性是不可避免的但关键在于你把复杂性放在哪里我们不能总想着“怎么简单怎么来”既要避免“过度设计”的陷阱也要拒绝“先有后优”的诱惑。而是要实事求是对未来可能发生的问题适当做一些前置思考既要考虑满足当下的任务需求又要对后续的扩展性留足演化空间。因此架构设计的思想应该贯穿整个项目的开发周期而绝不仅仅是“搭个应该启动框架”就叫架构设计了而对于这次的权限模块开发案例结合深度使用Blazor这个框架我对架构设计在这个项目中的体会主要有两个。1. 边界感首先就是体会到系统设计和代码开发之间的边界感对全栈工程师来说你很难提前把一切都想明白之后再去动手写代码很多时候都是边想边做做着做着就悟了而明悟的那个感觉就是边界感。以此次项目为例架构设计的核心任务之一是收拢复杂度如果权限逻辑散落在 100 个页面里那不是简单而是灾难通过DataScopeFilter我将复杂的嵌套 And/Or 逻辑封装在底层。这种局部的复杂性换取了全局的健壮性。2. Blazor 的组件哲学回到Blazor架构本身我在上一篇介绍它的博客里安利一下Blazor.NET开发者的全栈“优”选项就深刻感受到 Blazor 的设计哲学与 Vue、React 等前端框架有着跨越框架的共鸣深度使用之后我对这个观点更加肯定。Blazor 并不是在强行把后端逻辑塞进浏览器而是完全遵循了现代组件化的 UI 逻辑。数据传递Blazor 的[Parameter]完美对标 React/Vue 中的Props定义了单向数据流的入口。事件回调Blazor 的EventCallback 对应 Vue 的 $emit 或 React 的回调函数确保了组件状态变更的可追溯性。依赖注入通过 CascadingParameters级联参数我们可以像使用 React 或 Vue 的Provide/Inject 一样在深层组件树中优雅地共享权限状态。这种高度的一致性意味着.NET 开发者做全栈开发不必再执着于引入 Vue 或 React。 逻辑底层是完全相通的你不仅能享受 C# 强大的类型系统还能无缝应用现代前端的设计思想换句话说你不必担心被Blazor套牢。总结本来只是想聊聊在 Blazor 里怎么实现一个细粒度的权限过滤模块结果一不小心聊到了架构、复杂度甚至还“跨界”对比了前端框架……好了就聊这么多下次继续。