已发布的《权限控制——后台系统的核心命脉》讲的是 RBAC 的概念和用法。这篇往上走一层讲 2.3 源码里权限到底是怎么判的一条路由为什么会被拦、一个按钮为什么时有时无、服务端在哪些地方又校验了一遍。一、权限模型只有三层但每层都要用对EasyAdminBlazor 的权限模型很朴素SysUser ──(SysRoleUser)── SysRole ──(SysRoleMenu)── SysMenu ├── Type 菜单 → 能进这个页面吗 └── Type 按钮 → 能点这个操作吗对应的实体实体作用关键字段SysUser用户OrgId数据权限用、Roles多对多SysRole角色IsAdministrator、DataPermission、CustomDataPermissionSysMenu菜单 / 按钮 / 外链 / 增删改查Path、PathLower、ParentId、TypeSysRoleUser用户-角色关联UserId、RoleIdSysRoleMenu角色-菜单关联RoleId、MenuId菜单类型决定了它在授权里扮演什么角色publicenumSysMenuType{[Display(Name菜单)]Menu,[Display(Name按钮)]Button,[Display(Name外部链接)]ExternalLink,[Display(Name增删改查)]Crud}这里有个经常被忽略的设计按钮也是菜单。它和页面共用一张表、一套关联关系只是Type不同、Path是add/edit/remove这样的动作码。这样角色能看哪些页、能点哪些按钮就是一次勾选能解决的事不需要第二套权限表。框架初始化时会写入标准的增删改查按钮// EasyAdminBlazor/SeedData.csListSysMenugetCudButtons(paramsSysMenu[]btns){varlistnewListSysMenu{newSysMenu{Label添加,Pathadd,Sort10011,TypeSysMenuType.Button,IsSystemtrue},newSysMenu{Label编辑,Pathedit,Sort10012,TypeSysMenuType.Button,IsSystemtrue},newSysMenu{Label删除,Pathremove,Sort10013,TypeSysMenuType.Button,IsSystemtrue}};...}也就是说按钮权限码在框架里是约定好的字符串add/edit/remove。自定义动作可以自己加比如角色页的alloc_menus。二、登录后权限是怎么加载的1. InitRoles从数据库到内存AdminContext.InitRoles()是权限的加载入口publicasyncTaskInitRoles(){if(Usernull)return;varversionawaitGetPermissionVersionAsync();Rolesawaitcache.GetOrCreateAsyncListSysRole(ScopedUserRolesKey(version),async(){returnawaitOrm.SelectSysRole().Where(aOrm.SelectSysRoleUser().Any(bb.UserIdUser.Idb.RoleIda.Id)).ToListAsync();},TimeSpan.FromMinutes(30))??[];RoleMenusawaitcache.GetOrCreateAsyncListSysMenu(ScopedUserRoleMenusKey(version),async(){if(Roles.Any(aa.IsAdministrator)){returnawaitOrm.SelectSysMenu().ToListAsync();}varroleIdsRoles.Select(aa.Id).ToList();varroleMenusawaitOrm.SelectSysMenu().Where(aOrm.SelectSysRoleMenu().Any(broleIds.Contains(b.RoleId)b.MenuIda.Id)).ToListAsync();varparentIdsroleMenus.Select(aa.ParentId).Distinct().ToList();if(parentIds.Count0){varparentMenusawaitOrm.SelectSysMenu().Where(aparentIds.Contains(a.Id)).AsTreeCte(up:true).ToListAsync();roleMenus.AddRange(parentMenus);}returnroleMenus.DistinctBy(aa.Id).ToList();},TimeSpan.FromMinutes(30))??[];}三个细节值得注意管理员走的是全部菜单不是勾选的菜单。只要角色上IsAdministrator trueRoleMenus就是整张菜单表。父菜单会自动补全。如果角色只勾了子菜单AsTreeCte(up: true)会把祖先链补上——否则导航树渲染不出来面包屑也会缺层级。结果进缓存 30 分钟。这就是为什么改了权限要重新登录或等缓存失效这个说法存在。2.3 用权限版本号把这个体验改好了见下一节。2. 权限版本号让缓存立即失效privateconststringPermissionVersionKeyPermissionVersion;privatestringScopedPermissionVersionKey${TenantCachePrefix}{PermissionVersionKey};privatestringScopedUserRolesKey(intversion)${TenantCachePrefix}UserRoles:{version}:{User!.Id};privatestringScopedUserRoleMenusKey(intversion)${TenantCachePrefix}UserRoleMenus:{version}:{User!.Id};publicasyncTaskInvalidatePermissionCacheAsync(){varversionawaitGetPermissionVersionAsync();awaitcache.SetAsync(ScopedPermissionVersionKey,version1,TimeSpan.FromDays(30));}思路很直接缓存键里带版本号版本号一涨所有用户的旧键就都读不到了等于全量失效。菜单、角色、用户角色的变更页面都会调用它页面调用点Pages/Menu.razor菜单新增/编辑/删除后Pages/Role.razor角色保存、分配菜单、删除后Pages/User.razor用户角色变更后缓存键还带了TenantCachePrefixtenant:{code}:所以租户之间不会互相清缓存、更不会串权限——这部分在多租户那篇会展开。三、路由级授权AuthPath1. 判定逻辑publicasyncTaskboolAuthPath(stringpath,stringbutton){if(Usernull||string.IsNullOrEmpty(path))returnfalse;pathRemoveAfterThirdSlash(path).ToLower().Trim(/);awaitInitRoles();if(Roles.Count0)returnfalse;CurrentMenuRoleMenus.FirstOrDefault(aa.PathLowerpath);if(new[]{admin,admin/login,admin/profile}.Contains(path))returntrue;if(CurrentMenu!null){CurrentMenu.ParentRoleMenus.FirstOrDefault(aa.IdCurrentMenu.ParentId)!;}if(button!){AuthPathSuccessCurrentMenu!null;returnAuthPathSuccessFindButtonRecursive([CurrentMenu],button.ToLower())!null;}returnAuthPathSuccessCurrentMenu!null;}逐条翻译成白话没登录直接拒绝。路径先做归一化RemoveAfterThirdSlash只保留前三段、转小写、去掉首尾斜杠。这是为了让/Admin/User/123这样的详情页能匹配到/Admin/User这个菜单。没角色直接拒绝。注意这里是Roles.Count 0就 false不是没有菜单才 false。白名单admin、admin/login、admin/profile恒放行。菜单匹配用PathLower存库时由审计自动写入小写值所以菜单路径大小写不敏感。匹配成功后顺手把Parent从RoleMenus里补上供面包屑渲染。第二个参数button非空时是路由 按钮联合判定返回值为按钮是否存在。2. 拦截点在哪MainLayout把OnAuthorizing交给了 BootstrapBlazor 的LayoutLayout ... MenusMenus OnAuthorizingOnAuthorizing ...asyncTaskboolOnAuthorizing(stringpath){// 等待初始化完成await_initializationCompleted.Task;if(!awaitadmin.CheckOtherLogin()){returntrue;}returnawaitadmin.AuthPath(newUri(path).AbsolutePath);}而页面主体渲染前还会再看一次结果Main CascadingValue Valuethis IsFixedtrue if (admin.AuthPathSuccess) { Body } /CascadingValue /Main另外MainLayout在OnAfterRenderAsync里注册了导航拦截locationChangingHandlernav.RegisterLocationChangingHandler(asynce{varpathnav.ToAbsoluteUri(e.TargetLocation).AbsolutePath.ToLower().Trim(/);if(!AdminOptions.Value.EnableMessagepath.Contains(message)){return;}IsFavoriteFavoriteMenus.Any(xx.Value.ToLower()path);awaitadmin.AuthPath(path);});所以路由级授权有两条防线进入页面时的OnAuthorizing以及后续每一次导航时的LocationChanging回调。3. 导航菜单也来自同一份数据侧边栏菜单不是写死的而是从RoleMenus里筛出来的SysMenuType[]menuTypes[SysMenuType.Menu,SysMenuType.Crud];vardataadmin.RoleMenus.Where(aa.IsHiddenfalse).Where(amenuTypes.Contains(a.Type)).OrderBy(aa.Sort).ToList();只渲染菜单和增删改查两种类型Button不会出现在侧边栏IsHidden的也不会。这就是权限即导航能看到的菜单和能进的页面是同一份数据决定的不会出现两处配置不一致。四、按钮级授权AuthButtonpublicasyncTaskboolAuthButton(stringpath){if(Usernull||CurrentMenunull)returnfalse;awaitInitRoles();if(Roles.Any(aa.IsAdministrator))returntrue;returnFindButtonRecursive([CurrentMenu],path.ToLower())!null;}注意它依赖CurrentMenu。CurrentMenu是AuthPath阶段确定的所以AuthButton的语义是当前页面下有没有这个按钮权限不是全局权限码。递归查找的实现privateSysMenu?FindButtonRecursive(ListSysMenufindMenus,stringpathLower){varparentIdsnewHashSetlong(findMenus.Select(mm.Id));varchildsRoleMenus.Where(aparentIds.Contains(a.ParentId)).ToList();if(childs.Count0)returnnull;varbuttonchilds.FirstOrDefault(aa.TypeSysMenuType.Buttona.PathLowerpathLower);if(button!null)returnbutton;returnFindButtonRecursive(childs,pathLower);}从当前菜单往下逐层找Type Button且路径匹配的节点。所以按钮必须挂在目标菜单的子节点下挂在别的菜单下不算数层级别太深虽然它是广度优先递归但按钮挂在子菜单下面才符合语义。谁在调用 AuthButton调用点用途AdminTable.OnParametersSetAsync决定ShowAddButton/ShowEditButton/ShowDeleteButton是否显示AdminTable.OnSaveDataAsync/OnDeleteDataAsync保存、删除前的服务端校验AdminTable.SaveWithPermissionAsync/DeleteWithPermissionAsync包装页面自定义回调AdminTable导入AuthButton(add)Pages/User.razor、Role.razor、Org.razor、Menu.razor页面内自定义按钮与二次校验AdminFilePicker.razorAuthPath(/Admin/File, add)/AuthPath(/Admin/File, remove)最后一行说明AuthPath(path, button)这个重载的用途跨页面判定别的页面的按钮权限。文件选择器要判断的不是当前页而是文件管理页的权限。五、服务端二次校验这才是安全边界界面上隐藏按钮只是体验真正的安全边界在服务端。2.3 里至少有三层。1. AdminTable 的写路径保存和删除都会先过AuthButton第 04 篇有完整代码varactionchangedTypeItemChangedType.Add?add:edit;if(!awaitadmin.AuthButton(action)){awaitMessageService.Error(string.Format(CommonLocalizer[没有权限执行该操作]));returnfalse;}2. AdminButtonAttribute给方法加权限框架提供了一个 AOP 特性基于 Rougamo 实现[AttributeUsage(AttributeTargets.Method)]publicclassAdminButtonAttribute:Rougamo.AsyncMoAttribute{publicstringName{get;set;}string.Empty;publicoverrideasyncValueTaskOnEntryAsync(MethodContextcontext){varservicecontext.GetServiceProvider();varadminservice.GetServiceAdminContext();varswalServiceservice.GetServiceSwalService();if(adminisnull){context.ReplaceReturnValue(this,context.ReturnType.CreateInstanceGetDefaultValue());return;}if(!awaitadmin.AuthButton(Name)){context.ReplaceReturnValue(this,CreateDeniedReturnValue(context.ReturnType));if(swalServiceisnotnull)awaitswalService.Warning(你没有权限执行该操作);}}}用法就是在服务方法上标权限码[AdminButton(edit)]privateasyncTaskAllocMenus(){...}被拦截时Taskbool返回false、Task返回已完成任务方法体不会执行。源码里对返回值类型的处理是有意保守的无法安全构造返回值的泛型TaskT它会返回null交给调用方处理。3. 真正的 HTTP 端点SysFileControllerBlazor Server 后台大部分操作走 SignalR 电路但文件下载这类必须走 HTTP因此授权要单独做。SysFileController里的私有文件判定privateasyncTaskboolIsAuthorizedAsync(SysFilefile){await_adminContext.InitRoles();if(_adminContext.Roles.Any(rr.IsAdministrator)){returntrue;}if(_adminContext.Roles.Any(rr.DataPermissionDataPermissionType.AllData)){returntrue;}returnfile.CreatedUserId_adminContext.User?.Id;}注意它的注释写得很清楚文件实体本身不实现IDataPermission所以这里用的是与数据权限一致的判定并且上传人信息缺失时按拒绝处理fail-closed。六、关于API 权限需要说实话的部分很多文章会把后台权限和API 权限混在一起讲。在 EasyAdminBlazor 的架构里需要分清楚1. 后台页面本身没有独立 API 层。页面是MapRazorComponentsApp()下的 Blazor 组件交互走 SignalR 电路不存在前端调 REST API这一步。所以授权点就是前面讲的路由AuthPath 操作AuthButton 服务方法AdminButtonAttribute。2. 需要对外提供 API 时要单独设计。框架提供的是登录票据认证处理器和 Cookie 认证Infrastructure/Security/EasyAdminLoginTicketAuthenticationHandler.cs、CookieOptionsHelper.cs以及 CSRF 防护EasyAdminAntiforgeryFilter.cs、app.UseAntiforgery()。如果你要暴露给第三方或前端 SPA 用需要自己加授权策略——这部分不要指望 Blazor 组件的权限判定自动生效。3. 自定义的 HTTP 端点要自己写授权。参考SysFileController的做法注入AdminContext调用InitRoles()再按角色/数据权限判定而不是只看是否登录。把这条讲清楚比随便写一句框架自带 API 权限要诚实得多。七、实战排查权限不对时按这个顺序查现象大概率原因查什么登录后直接跳回/Admin/AuthPath返回 false用户的角色是否勾了该菜单菜单Path是否和page一致忽略大小写菜单能看到点进去跳首页菜单类型不是Menu/Crud或IsHidden true菜单管理里的类型和隐藏开关添加/编辑/删除按钮不显示按钮子节点缺失或角色未勾该菜单下是否有add/edit/remove角色的菜单树是否包含按钮按钮显示了但保存失败提示没有权限按钮权限有但服务端判定用的页面上下文不对CurrentMenu是否是你以为的那个菜单是否存在同名路径改了权限要等一会儿才生效权限缓存变更页面是否调用了InvalidatePermissionCacheAsync当前 Circuit 是否需要重新登录详情页/Admin/User/123匹配不上路径截断规则RemoveAfterThirdSlash只保留前三段菜单必须注册在前两段如Admin/User八、小结2.3 的权限系统可以用一张图概括登录 → InitRoles()User → Roles → RoleMenus带缓存 版本号 → AuthPath(path)菜单级授权路由守卫 导航守卫 → AuthButton(code)按钮级授权UI 显隐 服务端校验 → AdminButtonAttribute服务方法级授权AOP → HTTP 端点自行授权参考 SysFileController穿透整条链路的关键认知只有一句菜单和按钮是同一张表页面能进、按钮能点、服务端放行用的是同一套数据。理解了这一点权限问题的排查就变成了这份数据里到底有没有这条记录。如果你正在用 .NET 10 Blazor 做后台权限是绕不开的基础设施。EasyAdminBlazor 把菜单、按钮、角色、数据权限做成了一套开箱可用的模型需要深度定制时也能顺着源码改。文档https://easyadmin.wang-zhan.com.cn/doc源码https://gitee.com/gudufy/EasyAdminBlazor
企业数字化 ERP 产品动态
相关推荐
Python批量图片采集实战:从链接提取到并发下载的完整指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 23:22:17
uBlock Origin广告拦截器实战:安装、使用与误伤排查 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 23:22:11
AURIX TriCore开发环境搭建:HighTec编译器与UDE调试器联调实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 23:22:11
WordPress页面模板目录文件下载:5大注意事项避坑指南 WordPress页面模板目录文件下载:5大注意事项避坑指南 域名解析报错?服务器连不上?别慌,这往往是新手搞混了本地环境与线上部署的边界。很多做网站的朋友,一上来就对着浏览器地址栏发呆,觉得“我明明下载了文件,为什么打不开?”其实,… · 2026/9/28 0:25:15
南宁网站建设公司利润真相:新手怎么选避坑指南 南宁网站建设公司利润真相:新手怎么选避坑指南 网站上线三个月,后台日志显示每天只有寥寥几个IP访问,全是蜘蛛或者自己点的。这种“网站做好了没人访问”的绝望感,是无数南宁老板和初创企业最真实的痛点。你花了几万甚至十几万做的站,成了公司官网里的… · 2026/9/28 0:25:15
新手入门外贸网站服务器推荐:3个坑避开,省钱又稳定 新手入门外贸网站服务器推荐:3个坑避开,省钱又稳定 域名解析报错?服务器连不上?对于刚接触外贸建站的新手来说,“域名服务器搞不懂”是比代码还劝退的第一道坎。很多人以为买个好域名就行,结果部署时才发现,选错服务器比选错颜色更致命。… · 2026/9/28 0:25:09
选对早教网站源码才不挂马,这套最佳实践救过我 选对早教网站源码才不挂马,这套最佳实践救过我 网站被黑挂马,后台乱发博彩广告,客户投诉电话打爆?别急着删库重装,那是治标不治本。很多老板觉得只要源码是“免费”的或者“破解”的就能省钱,结果因为代码里藏着后门,服务器成了肉鸡,域名直接进黑名单… · 2026/9/28 0:24:27
2026最新wordpress手机端侧面小工具避坑指南 2026最新wordpress手机端侧面小工具避坑指南 网站做好了没人访问?别急着怪流量,先看看你的手机端体验是不是劝退了用户。2026年,移动流量占比已超85%,如果用户打开你的wordpress站点,面对的是一堆挤在底部的杂乱链接,或者… · 2026/9/28 0:24:03
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现 简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01
汕头网站建设制作厂家避坑指南:5大注意事项救急 汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习 简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01
制作网页比较方便的软件怎么选?一文搞懂避坑指南 制作网页比较方便的软件怎么选?一文搞懂避坑指南 很多老板一上来就问:做个网站多少钱?但我反问他:你的域名买了吗?服务器租了吗?他一脸懵。这就是典型的“域名服务器搞不懂”。别急,今天咱们不聊虚的,直接 一文搞懂 那些让你头秃的技术名词。… · 2026/9/28 0:00:06
婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 找婚恋网站建站公司,最怕的就是被坑高价。很多同行跟我吐槽,报价单上写得模棱两可,功能栏里全是“高级定制”、“专属UI”,结果落地全是套壳。今天不聊虚的,直接甩几个我经手的 实战案例… · 2026/9/28 0:00:19
济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 上周济南一个做建材的老板找我,脸都绿了。他的官网首页弹出了赌博广告,后台被植入了挖矿脚本。他慌得问我:“网站被黑挂马不知道怎么办?能不能直接找之前的外包公司要源码下载,看看哪里被动了手脚?… · 2026/9/28 0:00:25