基于 Vue3、Vue Router 与 Pinia 的中后台权限架构实战:从路由/菜单/按钮/数据四级权限模型到工程化落地,彻底搞懂权限系统最佳写法,避开菜单路由割裂与前后端鉴权失配等高频坑。

📑 文章目录
- 一、为什么你总觉得"权限做完了",上线后还是到处漏?
- 二、先统一概念:角色、权限点、资源、策略
- 三、四级权限体系总览(建议架构)
- [四、项目基础:Vue3 + Vue Router + Pinia 示例(可直接改造)](#四、项目基础:Vue3 + Vue Router + Pinia 示例(可直接改造))
- [4.1 权限数据模型(先定标准,再写逻辑)](#4.1 权限数据模型(先定标准,再写逻辑))
- [4.2 路由元信息:把权限声明写在路由上](#4.2 路由元信息:把权限声明写在路由上)
- [4.3 Pinia 权限仓库:集中管理,避免散弹式逻辑](#4.3 Pinia 权限仓库:集中管理,避免散弹式逻辑)
- [4.4 路由守卫:控制"能不能进页面"](#4.4 路由守卫:控制“能不能进页面”)
- [4.5 菜单权限:不是"从路由抄一遍",而是"基于路由过滤"](#4.5 菜单权限:不是“从路由抄一遍”,而是“基于路由过滤”)
- [4.6 按钮权限:用指令最省心(页面更干净)](#4.6 按钮权限:用指令最省心(页面更干净))
- [4.7 数据权限:前端别硬编码"where 条件",要走统一策略](#4.7 数据权限:前端别硬编码“where 条件”,要走统一策略)
- 五、完整实战案例:用户管理页如何落地四级权限
- [六、前端权限设计的 8 条实战规范(建议直接贴团队规范)](#六、前端权限设计的 8 条实战规范(建议直接贴团队规范))
- 七、最容易踩的坑(你大概率已经踩过)
- 八、你可以直接复用的落地清单(Checklist)
- 九、给多年前端的一句"校准建议"
- 十、总结
- [🔍 系列模块导航](#🔍 系列模块导航)
- [📝 权限与菜单架构](#📝 权限与菜单架构)
- [📚 系列总览](#📚 系列总览)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
当你能写出规范、可维护的代码后,下一个真正的瓶颈,就是架构。
面对大型项目、复杂业务,你是否也会遇到:组件越写越乱、重复开发越来越多;需求一变全链路改动;不知道怎么分层、怎么抽象、怎么设计才能支撑长期迭代;想晋升、想带项目,却缺少架构思维。
这一系列《前端组件化与架构实战》,我会继续用大白话 + 真实业务场景 ,不讲玄学、不啃晦涩源码,只教你能落地、能抗复杂项目的架构思路。
帮你从「写页面的开发者」,真正升级为「能做架构、能带项目、能搞定复杂需求的前端工程师」。
一、为什么你总觉得"权限做完了",上线后还是到处漏?
很多项目里,权限只做了"页面能不能进",但实际上业务里至少有 4 层:
- 路由权限:你能不能访问这个页面
- 菜单权限:你在侧边栏能不能看到这个入口
- 按钮权限:你在页面里能不能点"新增/删除/导出"
- 数据权限:你看到的是全部数据,还是只看自己/本部门的数据
如果只做路由权限,问题会非常典型:
- 菜单显示了,但点进去 403(体验差)
- 按钮隐藏了,但接口没鉴权(安全事故)
- 页面能进,但查到不该看的数据(高风险)
- 前端权限逻辑分散在各页面,后期没人敢改(维护地狱)
核心结论先说:前端负责"体验与引导",后端负责"安全与最终裁决"。
前端权限永远不能替代后端鉴权。
[⬆ 返回目录](#⬆ 返回目录)
二、先统一概念:角色、权限点、资源、策略
为了后面不绕晕,先统一术语:
- 角色(Role) :如
admin、editor、auditor - 权限点(Permission Code) :如
user:add、user:delete、report:export - 资源(Resource):路由、菜单、按钮、数据范围
- 策略(Policy):角色拥有哪些权限点,以及数据范围规则
推荐你把权限点设计成字符串 code,不要只用布尔值,扩展性更好。
[⬆ 返回目录](#⬆ 返回目录)
三、四级权限体系总览(建议架构)
前后端协作建议流程:
- 用户登录,后端返回 token
- 前端用 token 拉当前用户信息:角色 + 权限点 + 数据范围
- 前端根据权限点生成可访问路由、可见菜单
- 页面内通过指令/函数控制按钮显示
- 请求数据时附带数据范围参数(或由后端根据 token 自动裁剪)
- 后端每个接口仍做强鉴权(最终安全兜底)
[⬆ 返回目录](#⬆ 返回目录)
四、项目基础:Vue3 + Vue Router + Pinia 示例(可直接改造)
下面给一套"能跑通思路"的完整示例,你可以按自己项目拆文件。
4.1 权限数据模型(先定标准,再写逻辑)
ts
// src/types/auth.ts
export interface UserInfo {
id: string
name: string
roles: string[] // 角色列表
permissions: string[] // 权限点列表,如 ['user:view', 'user:add']
dataScope: 'ALL' | 'DEPT' | 'SELF'
deptId?: string
}
为什么这样设计?
roles方便做粗粒度分组permissions做细粒度控制(按钮/操作级)dataScope专门处理"能看多少数据",不要和按钮混一起
[⬆ 返回目录](#⬆ 返回目录)
4.2 路由元信息:把权限声明写在路由上
ts
// src/router/modules/system.ts
import type { RouteRecordRaw } from 'vue-router'
const systemRoutes: RouteRecordRaw[] = [
{
path: '/system',
name: 'System',
component: () => import('@/layouts/BasicLayout.vue'),
meta: {
title: '系统管理',
icon: 'Setting',
requireAuth: true
},
children: [
{
path: 'user',
name: 'SystemUser',
component: () => import('@/views/system/user/index.vue'),
meta: {
title: '用户管理',
requireAuth: true,
// 建议统一使用 permission code
permission: 'user:view',
menu: true
}
},
{
path: 'role',
name: 'SystemRole',
component: () => import('@/views/system/role/index.vue'),
meta: {
title: '角色管理',
requireAuth: true,
permission: 'role:view',
menu: true
}
}
]
}
]
export default systemRoutes
建议:
meta.permission只放"访问该页面需要的权限点"meta.menu控制它是否参与菜单渲染- 不要把复杂 if-else 写死在组件里,统一由路由元信息驱动
[⬆ 返回目录](#⬆ 返回目录)
4.3 Pinia 权限仓库:集中管理,避免散弹式逻辑
ts
// src/store/modules/auth.ts
import { defineStore } from 'pinia'
import type { UserInfo } from '@/types/auth'
import { getCurrentUserApi } from '@/api/auth'
interface AuthState {
token: string
userInfo: UserInfo | null
}
export const useAuthStore = defineStore('auth', {
state: (): AuthState => ({
token: '',
userInfo: null
}),
getters: {
isLogin: (state) => !!state.token,
permissions: (state) => state.userInfo?.permissions || [],
roles: (state) => state.userInfo?.roles || [],
dataScope: (state) => state.userInfo?.dataScope || 'SELF'
},
actions: {
setToken(token: string) {
this.token = token
},
async fetchUserInfo() {
const data = await getCurrentUserApi()
this.userInfo = data
return data
},
hasPermission(code: string) {
if (!code) return true
if (this.roles.includes('admin')) return true // 超管兜底
return this.permissions.includes(code)
}
}
})
关键点:
- 提供统一
hasPermission,组件层直接复用 - 超管逻辑统一放这里,不要散落全项目
[⬆ 返回目录](#⬆ 返回目录)
4.4 路由守卫:控制"能不能进页面"
ts
// src/router/guard.ts
import router from './index'
import { useAuthStore } from '@/store/modules/auth'
const WHITE_LIST = ['/login', '/404']
router.beforeEach(async (to, _from, next) => {
const authStore = useAuthStore()
// 白名单直接放行
if (WHITE_LIST.includes(to.path)) {
return next()
}
// 未登录
if (!authStore.isLogin) {
return next(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
}
// 没有用户信息时先拉取
if (!authStore.userInfo) {
try {
await authStore.fetchUserInfo()
} catch (error) {
return next('/login')
}
}
// 页面级权限校验
const requiredPermission = to.meta?.permission as string | undefined
if (requiredPermission && !authStore.hasPermission(requiredPermission)) {
return next('/403')
}
next()
})
常见坑:
- 首次刷新直接判权限,但
userInfo还没拉到,导致误判 403 - 忘记保留
redirect,登录后回不去原页面
[⬆ 返回目录](#⬆ 返回目录)
4.5 菜单权限:不是"从路由抄一遍",而是"基于路由过滤"
ts
// src/utils/menu.ts
import type { RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/store/modules/auth'
export function filterMenuRoutes(routes: RouteRecordRaw[]): RouteRecordRaw[] {
const authStore = useAuthStore()
const loop = (list: RouteRecordRaw[]): RouteRecordRaw[] => {
return list
.filter((route) => {
// 只显示标记为 menu 的节点
if (!route.meta?.menu) return false
const code = route.meta?.permission as string | undefined
return !code || authStore.hasPermission(code)
})
.map((route) => ({
...route,
children: route.children ? loop(route.children) : []
}))
}
return loop(routes)
}
菜单和路由关系建议:
- 来源统一是路由表(避免两套配置不一致)
- 菜单渲染是"过滤后的路由"
- 不建议手写独立菜单 JSON 再维护一套权限规则(除非你们后端直接返回菜单树)
[⬆ 返回目录](#⬆ 返回目录)
4.6 按钮权限:用指令最省心(页面更干净)
ts
// src/directives/permission.ts
import type { Directive } from 'vue'
import { useAuthStore } from '@/store/modules/auth'
export const vPermission: Directive = {
mounted(el, binding) {
const authStore = useAuthStore()
const code = binding.value as string
if (!code) return
const ok = authStore.hasPermission(code)
if (!ok) {
el.parentNode?.removeChild(el)
}
}
}
注册:
ts
// src/directives/index.ts
import type { App } from 'vue'
import { vPermission } from './permission'
export function setupDirectives(app: App) {
app.directive('permission', vPermission)
}
页面使用:
html
<template>
<div class="toolbar">
<el-button v-permission="'user:add'" type="primary">新增用户</el-button>
<el-button v-permission="'user:delete'" type="danger">批量删除</el-button>
<el-button v-permission="'user:export'">导出</el-button>
</div>
</template>
注意:
- 按钮隐藏只是 UX,接口必须后端再校验一次
- 对于"禁用而非隐藏"的场景,可把指令改成
el.disabled = true
[⬆ 返回目录](#⬆ 返回目录)
4.7 数据权限:前端别硬编码"where 条件",要走统一策略
例如用户列表查询:
ts
// src/api/user.ts
import request from '@/utils/request'
interface UserQuery {
page: number
pageSize: number
keyword?: string
deptId?: string
ownerId?: string
}
export function getUserListApi(params: UserQuery) {
return request.get('/users', { params })
}
在页面里根据数据范围生成查询参数:
ts
// src/views/system/user/useUserQuery.ts
import { computed } from 'vue'
import { useAuthStore } from '@/store/modules/auth'
export function useUserQueryScope() {
const authStore = useAuthStore()
const scopeParams = computed(() => {
switch (authStore.dataScope) {
case 'ALL':
return {}
case 'DEPT':
return { deptId: authStore.userInfo?.deptId }
case 'SELF':
default:
return { ownerId: authStore.userInfo?.id }
}
})
return { scopeParams }
}
更推荐做法:
- 前端可传
scopeHint,但后端应以 token 内身份为准 - 防止用户手改请求参数越权查数据
[⬆ 返回目录](#⬆ 返回目录)
五、完整实战案例:用户管理页如何落地四级权限
业务需求:
- 只有
user:view才能进用户管理页(路由) - 菜单中只有有权限的人看到"用户管理"(菜单)
user:add/user:delete/user:export控按钮(按钮)- 普通员工只能看自己数据,部门经理看本部门,管理员看全部(数据)
你会得到什么效果?
- 没权限的人:菜单看不到、地址栏强进会被拦截到 403
- 有浏览权限但无删除权限的人:能看页面,删按钮不出现
- 有页面权限的人:看到的数据仍按数据范围裁剪
这就是"体验一致 + 安全闭环"。
[⬆ 返回目录](#⬆ 返回目录)
六、前端权限设计的 8 条实战规范(建议直接贴团队规范)
- 权限点命名统一 :
模块:动作,如user:add、order:audit - 路由权限声明化 :权限写进
meta,不要散落页面 if-else - 权限判断函数统一出口 :全项目只认
hasPermission() - 菜单来源单一:优先由路由过滤生成
- 按钮权限组件化/指令化:避免每页重复判断逻辑
- 数据权限单独建模:别混在按钮权限里
- 后端强鉴权不可省:前端任何限制都可被绕过
- 本地缓存要考虑过期:token 过期、权限变更后要强制刷新用户信息
[⬆ 返回目录](#⬆ 返回目录)
七、最容易踩的坑(你大概率已经踩过)
-
坑 1:只做前端隐藏按钮,不做后端鉴权
后果:抓包调用接口照样能删数据。
-
坑 2:菜单和路由两套权限规则
后果:菜单显示与页面可访问状态不一致。
-
坑 3:把角色写死在前端
后果:每新增角色都要发版,维护成本爆炸。
正解:前端尽量只认权限点,角色交给后端映射。
-
坑 4:刷新后权限丢失
后果:首屏白屏或误判 403。
正解:路由守卫里先恢复登录态,再拉取用户权限。
-
坑 5:数据权限在页面里拼条件
后果:多个页面实现不一致,容易越权。
正解:统一查询构造逻辑,后端再兜底裁剪。
[⬆ 返回目录](#⬆ 返回目录)
八、你可以直接复用的落地清单(Checklist)
- 路由
meta.permission完整声明 -
authStore提供hasPermission - 全局路由守卫完成登录态与页面权限判断
- 菜单基于路由过滤,不维护第二份配置
- 按钮权限用
v-permission(或AuthButton组件) - 数据权限有独立字段(如
dataScope) - 后端接口全部做权限校验与数据裁剪
- 权限变更支持即时生效(重新拉取用户信息)
[⬆ 返回目录](#⬆ 返回目录)
九、给多年前端的一句"校准建议"
如果你以前习惯"先把页面做出来,再补权限",从今天开始换成:
先定权限模型(路由/菜单/按钮/数据)→ 再写页面。
你会明显感受到:
- 需求变更时不再恐慌
- 代码结构稳定,可复用性更高
- 团队协作时"谁都能接手",而不是"只有作者懂"
[⬆ 返回目录](#⬆ 返回目录)
十、总结
权限不是"加几个 if 判断",而是前端工程化能力的分水岭。
这套四级权限体系的价值,不是炫技,而是让项目在真实业务里可维护、可扩展、可审计。
[⬆ 返回目录](#⬆ 返回目录)
🔍 系列模块导航
📝 权限与菜单架构
一、《前端权限架构设计:路由/菜单/按钮/数据 四级权限体系|权限与菜单架构篇》
二、《菜单架构设计:递归渲染、权限过滤、多级菜单与面包屑统一|权限与菜单架构篇》
三、《按钮级权限实现:自定义指令 + 权限 Store,统一权限控制|权限与菜单架构篇》
四、《数据级权限实现:行权限/列权限,前端过滤与后端协同|权限与菜单架构篇》
五、《多租户权限架构:租户隔离、权限继承,适配多租户场景|权限与菜单架构篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
[⬆ 返回目录](#⬆ 返回目录)
📚 系列总览
前端体系化学习完全体:基础 → 规范 → 架构 → 大厂面试
四套系列、百余篇高质量实战文,从入门到进阶,一站式补齐前端核心能力
- 前端基础实战系列 : 《前端基础实战:JS/TS与Vue体系化扫盲(47 篇完整目录 + 避坑)》
- 前端规范实战系列 : 《JS/TS/Vue 前端规范实战:从写对到写优,搞定中后台规范落地,打造可维护代码(40 篇全目录)》
- 前端架构实战系列:聚焦工程化、性能优化、可维护架构、中后台体系设计(持续更新中)
- 前端大厂面试系列:覆盖高频考点、手写题、项目深挖、简历与面试技巧(规划中)
每个系列完结后,都会整理成一篇完整导航文并附上直达链接,方便大家按顺序、体系化学习。
全套内容持续更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
前端的成长路径很清晰:
会写代码 → 写规范代码 → 做可扩展架构。
每一步,都是职业晋升的关键台阶。
后续我会持续输出组件化、配置驱动、权限架构、工程化、复杂业务实战干货,帮你真正建立架构思维,在工作与面试中更有竞争力。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇硬核内容。
我是 Eugene,与你一起从业务走向架构,搞定复杂项目,我们下篇干货见~