html标签不参与权限决策,必须基于后端结构化权限数据驱动渲染;hidden/display:none仅视觉隐藏,无法阻止越权访问,应通过条件渲染彻底移除无权元素,并配合服务端细粒度鉴权。

HTML 标签本身不参与权限决策,所有显隐逻辑必须基于后端下发的结构化权限数据驱动;硬编码、本地存储、DOM 操作隐藏等做法在大规模系统中必然导致越权、跳变、可访问性崩坏和测试盲区。
为什么不能用 hidden 或 style="display: none" 控制权限元素
二者都只是视觉掩藏,不改变 DOM 结构或语义,屏幕阅读器仍可读取、搜索引擎仍可索引、键盘焦点仍可抵达。更关键的是:用户打开 DevTools 删除 hidden 属性,或执行 document.getElementById('delete-btn').style.display = 'block',就能立刻激活被“隐藏”的功能。
-
hidden本质等价于display: none,不是安全机制,是无障碍退化行为 - 用 JS 动态设
el.style.display = 'none'前,若没校验后端返回的权限字段(如permissions.includes('user:delete')),等于把权限判断权交给了前端运行时环境 - 大规模系统中,这类写法会导致权限变更后无法批量刷新(比如管理员实时回收某人权限),页面需强制刷新才生效
动态渲染菜单和按钮的正确路径:只生成有权元素
不是“先全量渲染再遍历隐藏”,而是根据后端返回的菜单数组直接构造 DOM。这样既避免无意义节点残留,也天然规避可访问性和 SEO 风险。
- 后端接口(如
GET /api/user/menu)应返回扁平或嵌套结构,每个项含path、name、permission字段,例如:{"path":"/users","name":"用户管理","permission":"menu:user"} - 前端用
v-if(Vue)或{hasPermission(item.permission) && <menuitem></menuitem>}(React)做条件渲染,而非操作已有 DOM - 纯 JS 场景下,用
menuData.filter(...).map(...).join('')生成 HTML 字符串再innerHTML插入,确保无权项从 DOM 树中彻底消失
hasPermission 函数必须支持前缀匹配与缓存失效
真实业务中权限码常有层级关系(如 user:read、user:edit、user:delete),靠 === 精确匹配会快速失灵。
- 函数应同时支持:精确匹配(
permissions.includes(code))、前缀匹配(permissions.some(p => p.startsWith(code + ':')))、通配符(code === 'user:*' || permissions.includes('user:*')) - 权限数组不能只在登录时拉一次就存 localStorage —— 用户切换角色、管理员后台调整策略后,必须触发
refreshPermissions()并清空旧缓存 - 避免在组件内重复调用
hasPermission:建议封装为 React 的usePermissionHook 或 Vue 的全局指令,内部自动订阅权限变更事件
最易被忽略的环节:表单字段级控制与路由守卫联动
菜单和按钮只是表象,真正高危的是表单提交和 URL 直接访问。这两处一旦缺失服务端校验,前端所有显隐逻辑形同虚设。
- 字段禁用(
disabled)或只读(readonly)仅作提示,提交时后端必须校验每个字段是否在用户权限白名单内,否则攻击者可绕过前端直接 POST 修改任意字段 - 前端路由守卫(如 Vue Router 的
beforeEach)只能拦截导航,不能替代接口鉴权;即使菜单没显示/admin/logs,用户手动输入该 URL 后,后端仍需对GET /api/admin/logs做独立权限校验 - 所有 API 接口必须带统一鉴权中间件,且校验粒度要细到操作+资源实例(如
canDeleteOrder(orderId, userId)),而非仅判断角色字符串
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











