Skip to content
bitzorcas
中EN

Guide

Menu 可见性、树与导航投影

解释 admin 短路、角色授权模块码、祖先补齐、三种投影、用户缓存键,以及环、孤儿和非菜单父节点边界。

Last updated

Menu 的读取不是“查表后原样返回”,而是可见码解析、祖先补齐、投影与缓存四个阶段。每一阶段都有不同的安全和数据完整性语义。

1. 可见性决策

是否是否

currentUser.User.Roles

包含 admin?

null = 全量可见

角色集合为空?

空集合

Authorization: granted module codes

TreeBuilder

admin 按角色名、不区分大小写短路。它不是 permission code,也不核对 Authorization 中的模块关系。

2. 角色名到模块码

非管理员把当前用户角色名交给 IMenuStore.GetVisibleModuleCodesAsync,store 再调用 IAuthorizationAssignmentReader.GetGrantedModuleCodesAsync。关系行由 Authorization owner 管理,Menu 不直接读取其 persistence record。

可见码解析的真实边界
// ① 角色名来自已认证的 ICurrentUser,不接受查询参数中的任意角色。
var roles = currentUser.User.Roles;
if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
return null; // null 是“全部”,不是故障。
// ② 无角色明确返回空集合。
if (roles.Count == 0)
return Array.Empty<string>();
// ③ 其他情况只调用 Authorization owner 的窄读端口。
return await menuStore.GetVisibleModuleCodesAsync(roles, cancellationToken);

3. 祖先补齐

如果用户只获准 orders.detail,父导航仍需出现才能承载子节点。Builder 对每个可见 Code 找到行,再沿 ParentId 向上,把所有祖先 Id 加入结果。

不加入

获准叶节点

父节点

根节点

未获准兄弟节点

追溯用 resultIds.Add 遇到重复 Id 即停止,因此祖先追溯本身对重复路径有一定终止性;后面的递归建树却没有 visited 集合。

4. Code 查找边界

Builder 对非空 Code 调用 ToDictionary。数据库唯一索引通常阻止重复 Code,但历史脏数据、大小写排序规则差异或 adapter 语义仍可能导致异常。

可见码在目录中不存在时会被静默忽略;不会返回配置错误或诊断详情。这对删除旧模块有容错价值,也可能掩盖授权关系漂移。

5. 三种投影

方法输出过滤
GetFlatAsyncMenuListItem[]Enabled + 可见/祖先;保留非菜单行
GetTreeAsyncMenuNode[]同上;递归 Children
GetNavigationAsyncNavigationItem[]同上,再只保留 IsMenu=true

Flat 不含 Description、旧 MVC 字段、IsMenu 和 Scope。Tree 包含 Code、IsMenu、Enabled;Navigation 不含 Code、OrderSort、Scope 或 Enabled。

6. 树构建规则

根键使用 null/0 的归一化:加载时 ParentId ?? "0",入口调用 BuildChildren(null)。子节点按 OrderSort 升序。

安全消费树输出
// ① 服务端已经完成可见性过滤和祖先补齐。
var tree = await menuTreeBuilder.GetTreeAsync(currentUser, cancellationToken);
// ② 客户端不要根据 Name 反推权限;Code 才是稳定模块键。
foreach (var node in tree)
RenderNode(node.Id, node.Name, node.LinkUrl, node.Children);
// ③ 点击目标链接后,目标端点再次独立授权。
// 菜单响应不能充当 capability token。

7. 非菜单父节点陷阱

Navigation 在建 lookup 前先过滤 IsMenu=true。如果一个可见子节点是菜单,但父节点 IsMenu=false,子节点仍保留原 ParentId,却没有根路径,因此不会出现在最终导航。

Tree 与 Flat 仍能看到该行。这会造成三个读取 API 结果不一致,配置工具应在写入时阻止或明确支持这种结构。

8. 禁用父节点

store 只加载 Enabled 行。禁用父节点但不禁用子节点时,子节点可能成为孤儿;它不会从根树出现。Toggle 只修改目标行,并不级联子孙的 Enabled。

恢复父节点后,仍启用的子节点会重新出现。这是“投影隐藏”,不是状态级联。

9. 环与孤儿

BuildChildren 递归没有 visited 或最大深度。根可达的环会无限递归并可能触发 StackOverflow;完全脱离根的环则不会输出。当前写入又允许创建这些数据。

商业 GA 前应同时补:写侧事务内环检测、读侧 visited/depth 防御、启动完整性扫描、运维修复命令和异常指标。

10. 缓存键

三类缓存区域分别是 menu-tree、menu-flat、menu-nav,scope 为 User。segment:

  • admin 固定为 admin;
  • 其他用户对排序后的可见 Code 使用 System.HashCode,形成 visxxxxxxxx;
  • policy 是 CachePolicy.Medium(),当前 TTL 15 分钟;
  • 所有项带 menus area tag。

可见集合变化会形成新键,写操作广播会清理旧 tag。System.HashCode 不承诺跨进程持久稳定,但当前 key 只服务本地缓存生命周期。

11. 认证与资源授权

JWT authentication

GetNavigation

tree / flat / detail

menus.menu.view pipeline

role-module visibility filter

navigation data

target endpoint authorization

Navigation 需要认证,因为生成属性默认要求授权;它不实现 IAuthorizedRequest,因此不要求 Menu 管理查看权限。tree/flat/detail 同时需要认证与 view 资源权限。

12. 建议的边界测试

至少固定:admin 大小写短路、无角色空结果、授权叶节点补父级、未知 Code、非菜单父节点、禁用父节点、孤儿、自引用、两节点环、相同排序、缓存集合变化和目标端点独立拒绝。

当前自动化只直接覆盖 detail handler 的命中/未找到;上述树策略尚无专门单元测试。

13. 核查命令

Terminal window
# 阅读完整可见性与树算法。
sed -n '1,280p' src/Platform/Menu/*Application/MenuTreeBuilder.cs
sed -n '1,180p' src/Platform/Menu/*Application/MenuVisibilityResolver.cs
# 确认 navigation 端点仍由生成器 RequireAuthorization。
rg -n "RequireAuthorization.*true|Append\(\"\.RequireAuthorization" src/Framework/BitzOrcas.Endpoint* -g '*.cs'

模块总览 · 安全与 GA

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%