前端权限系统怎么做才不会写吐?我们项目踩过的 3 套失败方案总结

上线前两个月,我们的权限系统崩了三次。

不是接口没权限,而是:

  • 页面展示和真实权限不一致;
  • 权限判断写得四分五裂;
  • 权限数据和按钮逻辑耦合得死死的,测试一改就炸。

于是,我们老老实实把整个权限体系拆了重构,从接口到路由、到组件、到 v-permission 指令,走了一遍完整的流程。

结果:代码可维护,调试容易,后端调整也能快速兜底。

这篇文章不讲理论,只还原我们项目真踩过的 3 套失败方案和最终落地方案。


❌ 第一套:按钮级权限直接写死在模板里

当时我们的写法是这样的:

html 复制代码
<!-- 用户管理页 -->
<el-button v-if="authList.includes('user:add')">添加用户</el-button>

接口返回的是一个权限数组:

ts 复制代码
["user:add", "user:delete", "user:list"]

然后整个项目几十个地方都这么判断。

结果:

  • 不能重用,每个组件都判断一次;
  • 权限粒度变更就全崩,比如从 user:add 改成 user:add_user
  • 后端权限更新后,前端要全局搜索权限 key 改代码;

典型的"写起来爽,维护时哭"方案。


❌ 第二套:用 router.meta.permission 统一控制,结果太抽象

重构后我们尝试统一控制页面级权限:

ts 复制代码
// router.ts
{
  path: '/user',
  component: User,
  meta: {
    permission: 'user:list'
  }
}

再通过导航守卫:

ts 复制代码
router.beforeEach((to, from, next) => {
  const p = to.meta.permission
  if (p && !authList.includes(p)) {
    return next('/403')
  }
  next()
})

这个方案页面级权限是解决了,但组件级 / 按钮级 / 表单字段级全都失效了。

而且你会发现,大量页面是"同路由但不同内容区域权限不同",导致这种 meta.permission 方案显得太粗暴。


❌ 第三套:封装权限组件,结果被吐槽"反人类"

当时我们团队有人设计了一个组件:

ts 复制代码
<Permission code="user:add">
  <el-button>添加用户</el-button>
</Permission>

这个组件内部逻辑是:

ts 复制代码
const slots = useSlots()
if (!authList.includes(props.code)) return null
return slots.default()

结果:

  • 逻辑上看似没问题,但使用非常反直觉;
  • 特别是嵌套多个组件时,调试麻烦,断点打不进真实组件;
  • TypeScript 报类型错误,编辑器无法识别 slot 类型;
  • 更麻烦的是,权限失效的时候,组件不会渲染,开发环境都看不到是为什么!

最终方案:hook + 指令 + 路由统一层级设计

我们最后把权限体系重构为 3 层:

🔹1. 接口统一管理权限 key → 后端返回精简列表(扁平权限)

ts 复制代码
export type AuthCode =
  | 'user:add'
  | 'user:delete'
  | 'user:edit'
  | 'order:export'
  | 'dashboard:view'

服务端返回用户权限集,保存在 authStore(Pinia / Vuex / Context)中。


🔹2. 统一 Hook 调用:usePermission(code)

ts 复制代码
import { useAuthStore } from '@/store/auth'

export function usePermission(code: string): boolean {
  const store = useAuthStore()
  return store.permissionList.includes(code)
}

用法:

html 复制代码
<el-button v-if="usePermission('user:add')">添加用户</el-button>

这才是真正组件内部逻辑干净、容易复用、TS 支持的方案。


🔹3. 封装一个 v-permission 指令(可选)

ts 复制代码
app.directive('permission', {
  mounted(el, binding) {
    const authList = getUserPermissions() // 从全局 store 获取
    if (!authList.includes(binding.value)) {
      el.remove()
    }
  }
})

模板中使用:

html 复制代码
<el-button v-permission="'order:export'">导出订单</el-button>

适合动态组件、render 生成的按钮,不适合复杂嵌套逻辑,但实际项目中效果拔群。


🧪 页面级权限怎么做?

不再用 router.meta,而是把每个路由页封装为权限包裹组件:

html 复制代码
<template>
  <PermissionView code="dashboard:view">
    <Dashboard />
  </PermissionView>
</template>

权限组件内部处理:

  • 没权限 → 自动跳转 403
  • 有权限 → 渲染内容

这样即使权限接口变了,组件逻辑也统一保留,避免页面空白或者闪跳


权限这事,不是实现难,而是维护难。

最核心的不是你怎么控制显示,而是权限 key 的一致性、复用性、分层能力。

最终我们稳定版本满足了:

  • 页面、按钮、字段统一接入权限
  • 新增权限点只需要改枚举,不需要大改
  • 新人接手也能一眼看懂逻辑,能调试

📌 你可以继续看我的系列文章

相关推荐
2501_920931706 小时前
React Native鸿蒙跨平台采用ScrollView的horizontal属性实现横向滚动实现特色游戏轮播和分类导航
javascript·react native·react.js·游戏·ecmascript·harmonyos
0思必得08 小时前
[Web自动化] Selenium处理动态网页
前端·爬虫·python·selenium·自动化
东东5168 小时前
智能社区管理系统的设计与实现ssm+vue
前端·javascript·vue.js·毕业设计·毕设
catino8 小时前
图片、文件的预览
前端·javascript
2501_9209317010 小时前
React Native鸿蒙跨平台实现推箱子游戏,完成玩家移动与箱子推动,当所有箱子都被推到目标位置时,玩家获胜
javascript·react native·react.js·游戏·ecmascript·harmonyos
layman052810 小时前
webpack5 css-loader:从基础到原理
前端·css·webpack
半桔10 小时前
【前端小站】CSS 样式美学:从基础语法到界面精筑的实战宝典
前端·css·html
AI老李10 小时前
PostCSS完全指南:功能/配置/插件/SourceMap/AST/插件开发/自定义语法
前端·javascript·postcss
_OP_CHEN10 小时前
【前端开发之CSS】(一)初识 CSS:网页化妆术的终极指南,新手也能轻松拿捏页面美化!
前端·css·html·网页开发·样式表·界面美化
啊哈一半醒10 小时前
CSS 主流布局
前端·css·css布局·标准流 浮动 定位·flex grid 响应式布局