菜单必须由后端动态返回并校验权限,前端仅渲染有权项且路由守卫需二次校验。正确做法是登录后调用 /api/user/menu 获取带 permission 字段的菜单数组,前端用 v-if 或条件渲染,配合前缀匹配的 haspermission 函数,并监听 storage 事件同步多标签页权限。

菜单数据必须从后端接口获取,不能写死在HTML里
HTML 本身没有逻辑判断能力,硬编码菜单项(比如直接写 <li>用户管理</li>)等于把权限开关交到用户手上——改一下源码或 localStorage 就能绕过。真实权限必须由后端动态决定。
正确做法是:登录后调用类似 /api/user/menu 的接口,返回一个结构清晰的菜单数组,例如:
[{"path":"/user","name":"用户管理","icon":"user","permission":"menu:user"},{"path":"/order","name":"订单管理","permission":"menu:order:read"}]
- 后端需根据角色、部门、数据范围等策略过滤,不是简单查表就返回全部
- 每个菜单项必须带
permission字段,用于前端细粒度比对 - 不要返回空数组或默认 fallback 菜单——没权限就该什么也不显示
前端渲染必须用条件逻辑,而不是 display: none
拿到菜单数据后,常见错误是先完整渲染所有菜单项,再遍历 DOM 执行 el.style.display = 'none'。这会导致屏幕阅读器仍可读取、SEO 混乱、测试路径遗漏,且无法响应权限变更。
应该只创建当前用户有权看到的节点:
- Vue 场景下用
v-if="hasPermission(item.permission)" - React 场景下用
{hasPermission(item.permission) && <menuitem ...></menuitem>} - 纯 JS 场景下,基于菜单数组
.map()生成 HTML 字符串再插入,不生成无权项
禁用某菜单项时,优先设 aria-disabled="true" 和 tabindex="-1",而非视觉隐藏。
hasPermission 函数得支持前缀匹配和通配符
菜单权限不止是“有或没有”,还涉及层级关系。比如用户有 menu:order:detail 权限,但菜单配置的是 menu:order 或 menu:order:*,也应能匹配成功。
Orderly React SDK 钩子使用参考指南,包括 useOrderEntry、usePositionStream、useOrderbookStream、useCollateral 等。
函数内部建议这样写:
- 精确匹配:
permissions.includes(code) - 前缀匹配:
permissions.some(p => p.startsWith(prefix)) - 通配符支持(如
menu:order:*)需额外解析,不建议在前端做复杂正则,优先由后端展开为具体权限码返回
别把权限数组存在 localStorage 后就不管了——用户切换角色后,必须重新请求 /api/user/menu 并更新缓存,否则菜单不会实时变化。
路由跳转前必须做守卫校验,不能只靠菜单隐藏
菜单没显示某个入口,不代表用户不能手动输入 URL 访问。所以仅靠菜单渲染控制远远不够。
以 Vue Router 为例,在 router.beforeEach 中必须检查:
- 当前路由的
meta.permission是否存在于用户权限数组中 - 权限数据是否已加载(避免未登录时
permissions为空导致白屏) - 若不满足,
next('/403')或next('/login?redirect=' + to.fullPath),并清空本地凭证
注意:权限字段必须来自后端接口,不是从 JWT payload 里硬解出的 role 字段——那个值可能被篡改,且无法表达细粒度权限。
storage 事件,并在 handler 中触发菜单重拉和路由重校验。前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










