1. 场景背景
面试官提问: “在一个中大型后台管理系统中,不同角色(如管理员、运营、财务)登录后看到的侧边栏菜单是不一样的,甚至同一个页面上,管理员能看到‘删除’按钮,而普通员工看不到。请问你如何设计并实现这套权限系统?请从前端架构的角度谈谈你的实现流程。”
2. 核心考点
- 路由守卫(Navigation Guards) 的运用。
- 动态添加路由(addRoute) 的机制。
- 自定义指令(Custom Directives) 的实现。
- 后端菜单权限数据结构设计。
3. 综合解决方案
第一步:路由设计(静态与动态分离)
不要将所有路由都写在 routes 数组里。
- 公有路由(Constant Routes): 登录页、404 页面等,直接初始化。
- 私有路由(Async Routes): 包含
meta: { roles: ['admin'] }等信息的页面,等待权限过滤。
第二步:登录与获取权限(核心流程)
- 用户登录拿到
Token并存储。 - 在路由前置守卫
router.beforeEach中判断:如果用户已登录但没有用户信息(或权限列表),则调用后端接口。 - 关键点: 后端返回一个权限标识符列表(如
['order:list', 'order:delete'])或菜单树。
第三步:动态生成路由(addRoute)
- 前端根据后端返回的权限数据,在
Vuex/Pinia中过滤出当前用户有权访问的Async Routes。 - 使用
router.addRoute()动态将这些路由注入到路由实例中。 - 避坑: 注入后需要执行一次
next({ ...to, replace: true })来确保路由生效。
第四步:按钮级权限控制
对于页面内的操作按钮,使用 自定义指令。
JavaScript
// v-permission 指令实现
const permission = {
mounted(el, binding) {
const { value } = binding; // 获取指令绑定的权限值
const userPermissions = store.getters.permissions; // 获取用户拥有的权限列表
if (value && !userPermissions.includes(value)) {
el.parentNode && el.parentNode.removeChild(el); // 无权限则删除 DOM
}
}
}
4. 满分回答示例
回答要点: “我会采取 ‘前端定义路由表,后端返回权限标识’ 的方案来实现方案。
首先,在代码层面,我会将路由分为常量路由和异步动态路由。
- 动态挂载: 在全局路由守卫
beforeEach中,当用户登录后,我会请求后端接口获取其角色的‘权限码’数组。然后利用一个递归函数,将本地定义的动态路由表与权限码进行匹配,筛选出可访问的路由,最后通过router.addRoute动态添加到路由系统中。 - 菜单渲染: 侧边栏菜单直接根据 Vuex/Pinia 中存储的这部分过滤后的路由表进行
v-for渲染,从而保证 UI 与权限的一致性。 - 细粒度控制: 针对按钮权限,我会封装一个全局自定义指令
v-permission。这样在业务代码中,只需要写<button v-permission="'user:delete'">删除</button>即可。 - 安全加固: 除了前端控制,我还会强调必须配合后端接口拦截,因为前端的所有隐藏都只是交互层面的,真正的安全屏障在后端。”
5. 查漏补缺(进阶加分项)
- 路由硬链接访问: 如果用户手动在地址栏输入无权访问的 URL,由于
addRoute没注册该路由,会自动触发 404,这保证了安全性。 - 退出登录重置: 退出登录时,必须重置路由实例(或者直接
location.reload()),否则切换账号后,上一个账号的路由可能依然存在。