真正的权限边界必须落在后端,路由是否可访问由服务端返回的菜单结构决定,前端需用该列表做路由守卫、动态生成DOM并确保权限数据与渲染时序一致。

权限路由不能靠display:none或hidden控制
DOM节点存在但不可见,不等于用户无权访问。攻击者删掉hidden属性、手动触发click、或直接构造URL访问,都能绕过前端视觉遮蔽。真正的权限边界必须落在后端——路由是否可访问,由服务端返回的菜单结构决定,而非CSS样式。
常见错误现象:
- 用户角色变更后,旧路由仍能通过地址栏直接打开
- DevTools里删掉style="display: none",按钮立刻可点
- 后端未校验/api/orders/export接口权限,前端隐藏导出按钮毫无意义
- 后端接口(如
GET /api/user/menu)必须返回用户真实可访问的路由列表,含path、permission字段 - 前端用该列表做路由守卫(如Vue Router的
router.beforeEach),匹配失败则next('/403') - 不要在路由配置里写死
meta: { requiresAuth: true }这种泛化标记,它无法表达“仅编辑员可进/posts/edit”的细粒度逻辑
静态HTML中如何安全渲染权限菜单
静态站点没有服务端模板渲染能力,但依然可以做到“无权即无DOM”。关键不是隐藏,而是跳过生成。
使用场景:纯HTML + JS打包产物,部署在Nginx或CDN上,无SSR能力
- 后端返回扁平菜单数组,例如
[{"path":"/users","name":"用户管理","permission":"menu:user"}] - 前端用
menuData.filter(item => hasPermission(item.permission)).map(...)拼接<nav>HTML字符串 - 最后用
document.getElementById('menu').innerHTML = htmlString插入——无权项从不进入DOM树 - 避免用
v-if或{condition && <Item/>}这类框架语法,它们依赖运行时JS,而静态HTML需保证初始HTML就是干净的
hasPermission()函数必须每次调用都基于最新权限数据
权限判断不是一次初始化就能一劳永逸的事。最容易被忽略的是状态不同步:API返回了新权限,但hasPermission还在用缓存的老数组。
立即学习“前端免费学习笔记(深入)”;
典型陷阱:
- 权限数据加载完成前,hasPermission已执行,返回false导致菜单空白
- 用户切换角色后,allPermsSet对象没重建,新权限标识查不到
- 不要把权限数组闭包在函数内部,比如
const perms = [...]; export function hasPermission(p) { return perms.includes(p); } - 应设计为
function hasPermission(p, currentPerms = window.userPermissions || []) { return currentPerms.includes(p); } - 路由守卫、按钮渲染、API请求拦截等所有调用点,都显式传入当前最新权限数组
静态HTML里data-permission比disabled更可靠
disabled只对<button>、<input>等原生表单控件生效;对<div class="btn">或自定义组件完全无效。而data-permission是通用标记,配合统一校验函数,可覆盖全部交互节点。
- 给按钮加
data-permission="order:delete",而不是硬编码if (role === 'admin') {...} - 点击时统一拦截:
element.addEventListener('click', e => { if (!hasPermission(e.target.dataset.permission)) return; ... }) - 避免用
pointer-events: none模拟禁用——它不阻止键盘触发、无障碍阅读器仍会朗读、焦点仍可抵达 - 真正需要禁用语义时(如提交按钮),才用
disabled,且确保后端也做了对应接口鉴权
最复杂的点不在怎么写,而在怎么保证权限数据和DOM渲染的时序一致——API响应、JS执行、HTML插入,这三个环节只要有一个错位,就会出现“该显示没显示”或“不该显示却暴露”的情况。别依赖“页面加载完再拉权限”,要让权限成为渲染的前提条件,而不是事后补丁。



















