很多后台系统一开始只是“登录后看到菜单”,后来才发现权限问题远不止菜单:路由能不能直接访问、按钮要不要显示、接口失败怎么提示、数据范围如何表达、页面刷新后状态如何恢复。
本文给出一套 Vue 3 + Element Plus 后台的最小权限架构,适合中小团队从混乱状态走向可维护。
先给结论:权限至少要覆盖四层
| 层级 | 负责什么 | 前端职责 |
|---|---|---|
| 菜单权限 | 用户能看到哪些入口 | 渲染导航和侧边栏 |
| 路由权限 | 用户能进入哪些页面 | 守卫、重定向、403 页 |
| 按钮权限 | 用户能做哪些动作 | 控制操作入口 |
| 接口权限 | 最终是否允许执行 | 处理后端拒绝结果 |
前端权限提升体验,后端权限保证最终边界。两者不能互相替代。
一、权限数据不要散落在页面里
权限最好用统一结构表达:
interface PermissionState {
menus: string[]
actions: string[]
roles: string[]
}
const can = (code: string) => permission.actions.includes(code)
页面里不要到处写 role === 'admin'。角色是人的分组,权限是动作边界。长期看,基于权限码判断比基于角色判断更灵活。
二、菜单和路由可以同源,但不要完全等同
菜单用于展示入口,路由用于页面访问。某些页面可能不出现在菜单里,例如详情页、编辑页、结果页,但仍然需要权限判断。
建议路由 meta 中声明权限:
{
path: '/users',
component: UserList,
meta: { permission: 'user:view' }
}
路由守卫检查 meta,菜单根据同一套权限码过滤。这样可以保持一致,又不把菜单结构当成全部路由结构。
三、按钮权限要和业务状态一起判断
按钮能不能点,不只由权限决定,还由当前数据状态决定。
例如删除按钮:
const canDeleteUser = (row: User) => {
return can('user:delete') && row.status !== 'locked'
}
模板里只负责展示:
<el-button v-if="canDeleteUser(row)" link type="danger">删除</el-button>
把判断函数集中起来,后续规则变化时更容易维护。
四、接口拒绝要有统一处理
即使前端隐藏了按钮,用户仍可能通过旧页面、直接请求或权限变化触发接口拒绝。接口层要统一处理 401、403 和业务拒绝。
推荐策略:
- 401:登录失效,引导重新登录
- 403:权限不足,显示明确提示或进入 403 页
- 业务状态冲突:在页面内展示可理解原因
不要让每个页面自己随意处理权限失败,否则体验会不一致。
五、页面刷新后权限要可恢复
权限状态不能只存在内存中。刷新页面后,应用需要根据 token 或会话重新拉取用户信息和权限,再初始化路由和菜单。
初始化顺序建议:
读取登录态 -> 获取用户信息 -> 获取权限 -> 注册/过滤路由 -> 渲染布局
如果权限未加载就渲染页面,容易出现菜单闪烁、路由误拦截或按钮短暂显示。
六、失败案例:只隐藏菜单,直接访问 URL 仍可进入
一个内部系统根据权限隐藏了侧边栏菜单,但没有做路由守卫。用户复制旧链接后仍能进入页面,只是在调用接口时失败。体验上看像页面坏了,安全边界也不清晰。
修复方式:
- 路由 meta 声明页面权限
- 守卫在进入前判断权限
- 无权限进入 403 页
- 接口层继续处理 403 作为最终兜底
- 按钮权限只作为体验优化,不作为唯一边界
七、权限后台 Checklist
- 权限码是否统一管理,而不是散落在页面
- 菜单权限和路由权限是否使用同一套来源
- 非菜单页是否也声明权限
- 按钮权限是否结合业务状态判断
- 接口 401/403 是否统一处理
- 刷新页面后权限是否能恢复
- 是否有清晰的 403、空态和加载状态
结语
权限后台的核心不是“藏按钮”,而是让入口、页面、动作和接口形成一致边界。Vue 3 和 Element Plus 能快速搭出界面,但权限模型需要从一开始就清楚,否则页面越多,补丁越多。
延伸阅读:


