☰
Ant Design Pro of Vue 权限管理实战:路由、菜单与按钮控制
2026/10/14 1:14:07 网站建设 项目流程

1. 权限管理在后台系统里到底难在哪

后台管理系统做久了你会发现,真正让人头疼的从来不是页面画得多漂亮,而是谁能看到什么、谁能点什么。一个几十个页面的中后台项目,如果没有一套清晰的权限体系,上线之后就是灾难:运营能看到财务数据,实习生能删掉核心配置,改一个角色要翻十几个文件。Ant Design Pro of Vue 这套模板之所以被大量团队拿来做中后台底座,很大一部分原因就是它内置了一套相对完整的权限方案,但很多人拿到手只会照着文档跑一遍,真到自己项目里改需求就懵了。

这篇内容我打算把 Ant Design Pro of Vue 的权限管理从头到尾拆一遍,包括它的设计思路、路由权限和按钮权限怎么配合、动态菜单怎么生成、后端返回的权限数据怎么映射到前端、以及我在实际项目里踩过的那些坑。适合正在用这套模板做中后台的开发者,也适合想自己搭一套权限体系、拿它当参考的人。不管你是刚接触这套模板,还是已经用了一段时间但总觉得权限这块没理顺,应该都能从里面找到能直接抄的东西。

先说清楚一个前提:Ant Design Pro of Vue 的权限不是单一机制,而是路由级权限 + 菜单级权限 + 操作级权限三层叠加的结果。很多人搞混就是因为把这三层当成一回事,结果路由配了、菜单没出来,或者菜单出来了、按钮还能点。下面我会一层一层拆开讲,把每一层负责什么、数据从哪来、怎么串起来说透。

2. 权限体系的整体设计与思路拆解

2.1 三层权限各管什么

在动手改代码之前,先把这三层的职责边界划清楚,后面所有配置都是围绕这个分工展开的。

路由级权限管的是“这个页面能不能进”。用户直接敲 URL 或者从菜单点进去,路由守卫会判断当前用户有没有这个页面的访问权限,没有就拦下来。它是最外层的防线,决定了页面的可达性。

菜单级权限管的是“这个入口要不要显示”。侧边栏是根据路由表动态生成的,没有权限的路由不会渲染成菜单项。它和路由权限是同一份数据源的两种表现,路由权限决定能不能进,菜单权限决定看不看得见入口。

操作级权限管的是“页面里的按钮能不能点”。比如新增、删除、导出这些按钮,不同角色看到的按钮不一样。它通常通过一个权限指令或者权限判断函数来实现,粒度最细,也最容易被忽略。

这三层的关系可以这样理解:路由权限是门禁,菜单权限是门牌,操作权限是房间里的开关。门禁没通过,后面都免谈;门禁过了但门牌没挂,用户找不到入口;门牌挂了但开关没锁,用户进去乱按一样出事。

2.2 为什么选择前端控制而不是全靠后端

这里有个经常被争论的问题:权限到底该前端控制还是后端控制。我的答案是两者都要,但分工不同。

后端控制是底线。任何涉及数据的接口,后端都必须校验当前用户有没有权限操作这条数据,这是安全红线,前端怎么改都绕不过去。前端控制是体验。如果所有权限都等后端返回 403 再提示,用户会看到一堆报错,体验极差。前端提前把没权限的入口藏掉、按钮禁掉,用户根本不会去点那些他不能做的事。

Ant Design Pro of Vue 的权限方案本质上是前端做展示层控制,后端做数据层控制。前端拿到的权限数据(通常是角色或者权限码列表)来自后端登录接口,前端根据这份数据决定渲染什么。所以你会看到它的权限判断逻辑都集中在前端,但真正敏感的操作后端一定还会再校验一次。理解这一点很重要,否则你会误以为前端藏了按钮就安全了,那是自欺欺人。

2.3 权限数据的两种常见形态

实际项目里,后端返回的权限数据基本逃不出两种形态,选哪种直接决定了前端怎么写。

第一种是基于角色。后端返回一个角色标识,比如admin、editor、guest,前端维护一张角色到权限的映射表。这种方案简单直接,适合角色数量少、角色权限相对固定的系统。缺点是角色一多、权限一细,映射表会变得很难维护,而且改权限要改前端代码。

第二种是基于权限码。后端返回一个权限码列表,比如['user:add', 'user:delete', 'order:export'],前端只判断当前用户有没有某个码。这种方案灵活,权限的增删改都在后端配置,前端不用动。缺点是权限码数量可能很多,需要一套命名规范来管理。

Ant Design Pro of Vue 默认更偏向第一种,但它的checkPermission机制其实两种都能接。我在项目里更推荐第二种,尤其是中大型系统,因为权限调整是运营的日常操作,不该每次都拉上前端发版。后面讲实操的时候我会给出两种形态的具体接法。

3. 核心细节解析与实操要点

3.1 路由表里的 meta 到底怎么配

路由权限的入口在路由配置的meta字段里。Ant Design Pro of Vue 的路由表通常长这样,每个路由的meta里可以挂permission、title、icon这些信息。permission就是这一层的关键,它声明了访问这个路由需要什么权限。

{ path: '/user', name: 'User', meta: { title: '用户管理', icon: 'user', permission: ['admin'] }, component: () => import('@/views/user/Index') }

这里有个细节很多人会踩:permission配成数组还是字符串。模板里默认是数组,判断逻辑是“用户拥有的权限和路由要求的权限有交集就放行”。也就是说permission: ['admin', 'editor']表示 admin 或 editor 都能进。如果你只想要单一权限,配成['admin']就行,别配成字符串,否则判断逻辑可能对不上。

还有一个容易忽略的点是父路由和子路由的权限关系。如果父路由配了权限,子路由没配,那子路由会继承父路由的权限判断吗?答案是不会自动继承。Ant Design Pro of Vue 的路由守卫是逐个路由判断的,父路由的权限只作用于父路由本身。所以如果你有一组子页面需要同样的权限,要么每个子路由都配上,要么在守卫里做一层继承逻辑。我一般会在守卫里加一段:如果当前路由没配permission,就往上找最近的配了permission的父路由,用它的权限来判断。这样配置量能少一大半。

3.2 路由守卫的判断时机与顺序

路由守卫是权限控制真正生效的地方。Ant Design Pro of Vue 的守卫逻辑集中在permission.js里,核心流程是:判断有没有 token,没有就跳登录;有 token 就判断有没有拉过用户信息,没拉过就拉一次;拉完之后判断当前路由有没有权限,没权限就跳 403。

这个顺序不能乱。我见过有人把权限判断放在拉用户信息之前,结果用户信息还没回来,权限列表是空的,所有页面都被拦到 403。正确的顺序一定是先确保用户信息和权限数据到位,再做权限判断。

router.beforeEach(async (to, from, next) => { const token = storage.get('token') if (!token) { if (whiteList.includes(to.path)) return next() return next('/user/login') } if (!store.getters.userInfo) { try { await store.dispatch('getUserInfo') } catch (e) { storage.clear() return next('/user/login') } } // 用户信息到位后再判断权限 if (hasPermission(to)) { next() } else { next('/403') } })

注意:getUserInfo这个 action 里一定要把权限数据一起拉回来并写进 store,否则守卫里拿不到权限列表,判断必然出错。

3.3 动态菜单是怎么从路由表长出来的

侧边栏菜单不是手写的,而是根据路由表递归生成的。Ant Design Pro of Vue 里负责这件事的是SideMenu组件,它会遍历路由表,把有meta.title且没有hidden标记的路由渲染成菜单项。

这里的关键是菜单生成和权限过滤是同一份数据。也就是说,如果一个路由用户没权限,它既不会出现在菜单里,也进不去。这两件事是联动的,不需要你分别维护。很多人以为菜单要单独配一份权限,其实不用,路由表配好permission,菜单自动就过滤了。

但有个坑要注意:菜单的过滤发生在路由表生成之后。如果你用的是动态路由(根据权限动态 addRoute),那菜单要在路由添加完之后再生成,否则菜单里会缺项。我一般会在getUserInfo成功之后,先根据权限过滤出可访问的路由,addRoute进去,再触发菜单重新渲染。顺序错了菜单就是空的。

3.4 操作级权限的两种实现方式

按钮级权限有两种常见做法,各有适用场景。

第一种是权限指令。注册一个全局指令v-permission,传权限码进去,指令内部判断当前用户有没有这个码,没有就把元素从 DOM 里移除。

Vue.directive('permission', { inserted(el, binding) { const { value } = binding const permissions = store.getters.permissions || [] if (value && !permissions.includes(value)) { el.parentNode && el.parentNode.removeChild(el) } } })

用的时候直接<a-button v-permission="'user:add'">新增</a-button>,干净利落。

第二种是权限判断函数。在组件里用checkPermission('user:add')返回布尔值,配合v-if使用。这种方式更灵活,因为你可以用它控制的不只是按钮,还有表格列、表单字段、甚至整块区域。

computed: { canExport() { return this.$auth('order:export') } }

我的建议是两者都用。简单的按钮显隐用指令,复杂的条件渲染用函数。指令的好处是代码简洁,函数的好处是能参与更复杂的逻辑判断。别死磕一种。

4. 实操过程与核心环节实现

4.1 从登录到权限落地的完整链路

把前面这些串起来,一个完整的权限落地链路是这样的:

  1. 用户登录,后端返回 token 和用户基本信息
  2. 前端存 token,跳转到首页或目标页
  3. 路由守卫拦截,发现没有用户信息,调用getUserInfo
  4. getUserInfo请求后端,拿到用户角色和权限码列表,写进 Vuex
  5. 根据权限码过滤路由表,动态addRoute可访问的路由
  6. 菜单组件根据过滤后的路由表重新渲染
  7. 页面内的按钮通过指令或函数判断权限码,决定显隐

这条链路里,第 4 步和第 5 步是核心。第 4 步决定了权限数据从哪来,第 5 步决定了权限数据怎么变成可访问的路由。下面我把这两步的代码写细一点。

4.2 getUserInfo 里该做什么

getUserInfo不只是拉用户信息,它还要把权限数据处理好。我一般会在这个 action 里做三件事:拉数据、存数据、生成可访问路由。

async getUserInfo({ commit }) { const res = await api.getUserInfo() const { user, roles, permissions } = res.data commit('SET_USER', user) commit('SET_ROLES', roles) commit('SET_PERMISSIONS', permissions) // 根据权限生成可访问路由 const accessibleRoutes = filterAsyncRoutes(asyncRoutes, permissions) commit('SET_ROUTES', accessibleRoutes) return accessibleRoutes }

filterAsyncRoutes是个递归函数,遍历路由表,对每个路由判断meta.permission和当前权限列表有没有交集,有就保留,没有就剔除。如果路由有子路由,递归处理子路由,子路由全被剔除的话父路由也一并剔除。

function filterAsyncRoutes(routes, permissions) { const result = [] routes.forEach(route => { const tmp = { ...route } if (hasPermission(permissions, tmp)) { if (tmp.children) { tmp.children = filterAsyncRoutes(tmp.children, permissions) } result.push(tmp) } }) return result } function hasPermission(permissions, route) { if (route.meta && route.meta.permission) { return permissions.some(p => route.meta.permission.includes(p)) } return true }

提示:hasPermission里对没有配permission的路由默认放行,这是合理的,因为像登录页、404 页这种本来就不需要权限。但如果你希望默认拒绝,把return true改成return false就行,看你的安全策略。

4.3 动态路由的添加时机

拿到可访问路由之后,要把它addRoute进 router 实例。这一步的时机很关键,必须在路由守卫放行之前完成,否则用户跳转过去会发现路由不存在。

// 在 permission.js 的守卫里 if (!store.getters.routesLoaded) { const routes = await store.dispatch('getUserInfo') routes.forEach(route => { router.addRoute(route) }) // 加一个兜底路由,防止刷新后 404 router.addRoute({ path: '*', redirect: '/404' }) store.commit('SET_ROUTES_LOADED', true) next({ ...to, replace: true }) }

这里有个经典坑:动态添加路由后,直接next()会白屏。因为当前这次导航是在路由添加之前发起的,路由表里还没有新路由。解决办法是next({ ...to, replace: true }),重新触发一次导航,这次路由表里已经有了,就能正常匹配。这个坑我踩过不止一次,每次都要愣一下才想起来。

4.4 按钮权限的实战配置

按钮权限的配置我建议统一走权限码,命名规范用模块:操作的格式,比如user:add、user:delete、order:export。这样一眼就能看出这个码是干什么的,也方便后端配置。

在组件里用的时候,指令和函数配合:

<template> <div> <a-button v-permission="'user:add'" type="primary">新增用户</a-button> <a-button v-if="canDelete" type="danger">删除</a-button> </div> </template> <script> export default { computed: { canDelete() { return this.$auth('user:delete') } } } </script>

$auth是个挂到 Vue 原型上的方法,内部就是判断权限码在不在当前用户的权限列表里。这样写的好处是,权限判断的逻辑只有一处,改起来方便。

5. 常见问题与排查技巧实录

5.1 权限相关的典型问题速查

问题现象可能原因排查方向
登录后白屏动态路由添加后没重新导航检查是否用了next({ ...to, replace: true })
菜单项缺失菜单在路由添加前生成确保菜单渲染在addRoute之后
刷新后 404动态路由没持久化刷新时重新拉权限并addRoute
按钮该藏没藏权限码不匹配对比后端返回的码和前端判断的码
所有页面都 403权限数据没拉到检查getUserInfo是否成功、store 是否有值
父路由有权限子路由进不去子路由没配权限且没继承在守卫里加父路由权限继承逻辑

5.2 刷新丢权限这个坑怎么填

动态路由最大的问题就是刷新之后路由表重置。因为addRoute是运行时加的,刷新页面 router 实例重新初始化,之前加的路由全没了。这时候用户如果停留在某个动态路由页面,刷新后就会 404。

解决办法是在守卫里判断:如果当前有 token 但路由没加载过,就重新走一遍getUserInfo和addRoute流程。这就是前面代码里routesLoaded这个标记的作用。第一次进来加载一次,之后就不再重复加载。刷新的时候标记是 false,会重新加载,加载完再next({ ...to, replace: true })重新导航,页面就能正常显示了。

注意:routesLoaded这个标记不要持久化到 localStorage,它应该是内存级的。持久化了刷新后还是 true,就不会重新加载路由,照样 404。

5.3 权限码和角色混用的处理

实际项目里经常出现角色和权限码混用的情况,比如有些页面按角色控制,有些按权限码控制。这时候判断逻辑要能同时处理两种。

我的做法是统一转成权限码。后端返回角色的时候,前端维护一张角色到权限码的映射表,把角色展开成权限码列表,之后所有判断都只认权限码。这样判断逻辑只有一套,不会出现“这里判断角色那里判断码”的混乱。

const rolePermissionMap = { admin: ['user:add', 'user:delete', 'order:export'], editor: ['user:add', 'order:export'], guest: [] } function expandRoles(roles) { const codes = new Set() roles.forEach(role => { (rolePermissionMap[role] || []).forEach(code => codes.add(code)) }) return [...codes] }

这样getUserInfo里拿到角色后先展开成权限码,再存进 store,后面所有判断都基于权限码,逻辑就统一了。

5.4 几个我踩过的坑

第一个坑是权限码大小写。后端返回User:Add,前端判断user:add,怎么都对不上。这种问题排查起来特别费劲,因为肉眼看着一样。建议前后端约定好全小写,或者判断的时候统一转小写。

第二个坑是异步权限判断。有人把权限判断写成异步的,等接口返回再决定按钮显不显示,结果按钮先渲染出来闪一下再消失。权限数据应该在进入页面前就准备好,页面内的判断都是同步的,不要临时去请求。

第三个坑是403 页面本身被拦。403 页面和 404 页面要加进白名单,否则用户被拦到 403 的时候,403 页面自己又需要权限,就死循环了。这个坑很隐蔽,因为正常流程走不到,只有权限出问题的时候才会暴露。

6. 权限方案的扩展与优化方向

6.1 细粒度到数据级的权限

前面讲的都是页面和按钮级别的权限,再往下还有数据级权限,比如“销售只能看自己负责的客户”。这种权限前端做不了主,必须后端在查询的时候带上用户维度过滤。前端能做的是把用户的身份信息传过去,后端根据身份决定返回哪些数据。

如果非要在前端做一层展示,可以在表格渲染的时候根据数据里的归属字段判断是否显示某些操作按钮。但这只是体验优化,真正的数据隔离还得靠后端。

6.2 权限配置的可视化

权限码多了之后,靠手写配置容易出错。我见过一些团队做了权限配置后台,运营在界面上勾选角色对应的权限码,保存后写进数据库,前端登录时拉取。这样权限调整不用改代码,运营自己就能搞定。如果你们的系统权限变动频繁,值得投入做这么一套。

6.3 权限缓存与更新

权限数据一般缓存在 store 里,页面刷新会重新拉。但如果用户在系统使用过程中权限被管理员改了,前端不会自动感知。简单的做法是提供一个“刷新权限”的入口,用户手动触发重新拉取。复杂一点可以做长连接推送,权限变更时主动通知前端刷新。大多数中后台系统用前者就够了,别过度设计。

权限这块东西,说复杂也复杂,说简单也简单,核心就是数据从哪来、怎么存、怎么用这三件事。Ant Design Pro of Vue 给的是一套骨架,真正贴合业务的肉还得自己填。我个人的经验是,前期把权限码规范定好、把动态路由的加载时机理顺,后面基本不会出大问题。最怕的就是一开始图省事,角色权限到处硬编码,等到系统大了再想重构,那成本就高了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询