前端权限架构设计:路由/菜单/按钮/数据 四级权限体系|权限与菜单架构篇

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

📑 文章目录

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)

当你能写出规范、可维护的代码后,下一个真正的瓶颈,就是架构

面对大型项目、复杂业务,你是否也会遇到:组件越写越乱、重复开发越来越多;需求一变全链路改动;不知道怎么分层、怎么抽象、怎么设计才能支撑长期迭代;想晋升、想带项目,却缺少架构思维

这一系列《前端组件化与架构实战》,我会继续用大白话 + 真实业务场景 ,不讲玄学、不啃晦涩源码,只教你能落地、能抗复杂项目的架构思路。

帮你从「写页面的开发者」,真正升级为「能做架构、能带项目、能搞定复杂需求的前端工程师」。


一、为什么你总觉得"权限做完了",上线后还是到处漏?

很多项目里,权限只做了"页面能不能进",但实际上业务里至少有 4 层:

  1. 路由权限:你能不能访问这个页面
  2. 菜单权限:你在侧边栏能不能看到这个入口
  3. 按钮权限:你在页面里能不能点"新增/删除/导出"
  4. 数据权限:你看到的是全部数据,还是只看自己/本部门的数据

如果只做路由权限,问题会非常典型:

  • 菜单显示了,但点进去 403(体验差)
  • 按钮隐藏了,但接口没鉴权(安全事故)
  • 页面能进,但查到不该看的数据(高风险)
  • 前端权限逻辑分散在各页面,后期没人敢改(维护地狱)

核心结论先说:前端负责"体验与引导",后端负责"安全与最终裁决"。

前端权限永远不能替代后端鉴权。

[⬆ 返回目录](#⬆ 返回目录)


二、先统一概念:角色、权限点、资源、策略

为了后面不绕晕,先统一术语:

  • 角色(Role) :如 admineditorauditor
  • 权限点(Permission Code) :如 user:adduser:deletereport:export
  • 资源(Resource):路由、菜单、按钮、数据范围
  • 策略(Policy):角色拥有哪些权限点,以及数据范围规则

推荐你把权限点设计成字符串 code,不要只用布尔值,扩展性更好。

[⬆ 返回目录](#⬆ 返回目录)


三、四级权限体系总览(建议架构)

前后端协作建议流程:

  1. 用户登录,后端返回 token
  2. 前端用 token 拉当前用户信息:角色 + 权限点 + 数据范围
  3. 前端根据权限点生成可访问路由、可见菜单
  4. 页面内通过指令/函数控制按钮显示
  5. 请求数据时附带数据范围参数(或由后端根据 token 自动裁剪)
  6. 后端每个接口仍做强鉴权(最终安全兜底)

[⬆ 返回目录](#⬆ 返回目录)


四、项目基础: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 条实战规范(建议直接贴团队规范)

  1. 权限点命名统一模块:动作,如 user:addorder:audit
  2. 路由权限声明化 :权限写进 meta,不要散落页面 if-else
  3. 权限判断函数统一出口 :全项目只认 hasPermission()
  4. 菜单来源单一:优先由路由过滤生成
  5. 按钮权限组件化/指令化:避免每页重复判断逻辑
  6. 数据权限单独建模:别混在按钮权限里
  7. 后端强鉴权不可省:前端任何限制都可被绕过
  8. 本地缓存要考虑过期:token 过期、权限变更后要强制刷新用户信息

[⬆ 返回目录](#⬆ 返回目录)


七、最容易踩的坑(你大概率已经踩过)

  • 坑 1:只做前端隐藏按钮,不做后端鉴权

    后果:抓包调用接口照样能删数据。

  • 坑 2:菜单和路由两套权限规则

    后果:菜单显示与页面可访问状态不一致。

  • 坑 3:把角色写死在前端

    后果:每新增角色都要发版,维护成本爆炸。

    正解:前端尽量只认权限点,角色交给后端映射。

  • 坑 4:刷新后权限丢失

    后果:首屏白屏或误判 403。

    正解:路由守卫里先恢复登录态,再拉取用户权限。

  • 坑 5:数据权限在页面里拼条件

    后果:多个页面实现不一致,容易越权。

    正解:统一查询构造逻辑,后端再兜底裁剪。

[⬆ 返回目录](#⬆ 返回目录)


八、你可以直接复用的落地清单(Checklist)

  • 路由 meta.permission 完整声明
  • authStore 提供 hasPermission
  • 全局路由守卫完成登录态与页面权限判断
  • 菜单基于路由过滤,不维护第二份配置
  • 按钮权限用 v-permission(或 AuthButton 组件)
  • 数据权限有独立字段(如 dataScope
  • 后端接口全部做权限校验与数据裁剪
  • 权限变更支持即时生效(重新拉取用户信息)

[⬆ 返回目录](#⬆ 返回目录)


九、给多年前端的一句"校准建议"

如果你以前习惯"先把页面做出来,再补权限",从今天开始换成:

先定权限模型(路由/菜单/按钮/数据)→ 再写页面。

你会明显感受到:

  • 需求变更时不再恐慌
  • 代码结构稳定,可复用性更高
  • 团队协作时"谁都能接手",而不是"只有作者懂"

[⬆ 返回目录](#⬆ 返回目录)


十、总结

权限不是"加几个 if 判断",而是前端工程化能力的分水岭。

这套四级权限体系的价值,不是炫技,而是让项目在真实业务里可维护、可扩展、可审计

[⬆ 返回目录](#⬆ 返回目录)


🔍 系列模块导航

📝 权限与菜单架构

一、《前端权限架构设计:路由/菜单/按钮/数据 四级权限体系|权限与菜单架构篇》

二、《菜单架构设计:递归渲染、权限过滤、多级菜单与面包屑统一|权限与菜单架构篇》

三、《按钮级权限实现:自定义指令 + 权限 Store,统一权限控制|权限与菜单架构篇》

四、《数据级权限实现:行权限/列权限,前端过滤与后端协同|权限与菜单架构篇》

五、《多租户权限架构:租户隔离、权限继承,适配多租户场景|权限与菜单架构篇》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~

[⬆ 返回目录](#⬆ 返回目录)

📚 系列总览

前端体系化学习完全体:基础 → 规范 → 架构 → 大厂面试

四套系列、百余篇高质量实战文,从入门到进阶,一站式补齐前端核心能力

每个系列完结后,都会整理成一篇完整导航文并附上直达链接,方便大家按顺序、体系化学习。

全套内容持续更新中,敬请期待~

[⬆ 返回目录](#⬆ 返回目录)


前端的成长路径很清晰:

会写代码 → 写规范代码 → 做可扩展架构。

每一步,都是职业晋升的关键台阶。

后续我会持续输出组件化、配置驱动、权限架构、工程化、复杂业务实战干货,帮你真正建立架构思维,在工作与面试中更有竞争力。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇硬核内容。

我是 Eugene,与你一起从业务走向架构,搞定复杂项目,我们下篇干货见~

相关推荐
SL-staff3 小时前
规则引擎技术选型指南:从开源Drools到企业级方案,架构演进与私有化实践
架构·开源·私有化部署·架构设计·规则引擎·drools·jvs-rules
fengxin_rou3 小时前
【后端配置模块实战】:索引、中间件与缓存架构全解析
缓存·中间件·架构
LaughingZhu3 小时前
Product Hunt 每日热榜 | 2026-05-27
前端·人工智能·经验分享·html
穗余4 小时前
2026 AI x Web3 School共学营笔记-Day9-隐私是需要理解的基础能力
学习·安全·架构
阿坤带你走近大数据12 小时前
数仓架构的设计思路、模型选择依据、落地难点及解决方案的介绍
架构·管理·数仓·业务与技术融合
ftpeak12 小时前
Mooncake:以 KVCache 为中心的分离式 LLM 服务架构
人工智能·ai·架构·ai编程·ai开发
ZC跨境爬虫12 小时前
跟着 MDN 学CSS day_16:(深入掌握背景与边框的艺术)
前端·css·ui·html·tensorflow
Agent手记15 小时前
制造业生产流程自动化,Agent需要具备哪些能力?深度拆解2026工业级智能体落地范式与核心架构
大数据·人工智能·ai·架构·自动化
道里15 小时前
花了 5 万刀用 AI 写代码之后,这是我的全部经验
前端·人工智能