前端如何实现权限控制?看这一篇就够了

基本概念

权限控制,最常见的基本上有 2 种

  • 基于 ACL 的权限控制

  • 基于 RBAC 的权限控制

这个两种到底有什么不同呢?

我们通过下图来分析一下

图片

ACL 是基于 用户 -> 权限,直接为每个用户分配权限

RBAC 基于 用户 -> 角色 -> 权限,以角色为媒介,来为每个用户分配权限

这样做的好处是,某个权限过于敏感时,想要将每个用户或者部分用户的权限去掉,就不需要每个用户的权限都操作一遍,只需要删除对应角色的权限即可

那在实际的开发中 RBAC 是最常用的权限控制方案,就前端而言,RBAC 主要如何实现的呢?

主要就两个部分

  • 页面权限受控

  • 按钮权限受控

下面我们就来实现这两个部分

  • 页面权限

  • 按钮权限

页面的访问,我们都是需要配置路由表的,根据配置路由表的路径来访问页面

那么,我们控制了路由表,不就能控制页面的访问了吗?

实现思路

  • 前端根据不同用户信息,从后端获取该用户所拥有权限的路由表

  • 前端动态创建路由表

基本环境:

创建项目

 npm install -g @vue/cli
 vue --version # @vue/cli 5.0.8
 vue create vue-router-dome

图片

打开项目,npm run serve 运行一下

代码初始化,删除不必要的一些文件

图片

我们创建几个新文件夹

图片

写下基本的页面

图片

 ​​​​​​

<!-- home.vue --><template>  <div>主页</div></template>
<!-- menu.vue --> <template>   <div>菜单管理</div> </template>
 <!-- user.vue --> <template>   <div>用户管理</div> </template>

写下路由配置

图片

  // remaining.ts
import Layout from '@/layout/index.vue'  const remainingRouter: AppRouteRecordRaw[] = [   {     path: '/remaining',     component: Layout,     redirect: 'home',     children: [       {         path: '/remaining/home',         component: () => import('@/views/home.vue'),         name: '首页',         meta: {},       }     ],     name: '主页管理',     meta: undefined   }, ]  export default remainingRouter

remaining 主要为了存放一些公共路由,没有权限页可以访问,比如登录页、404页面这些

因为是用 typescript 编写的,我们需要加一下声明文件,定义下 remainingRouter 的类型

 // router.d.ts import type { RouteRecordRaw } from 'vue-router' import { defineComponent } from 'vue'  declare module 'vue-router' {   interface RouteMeta extends Record<string | number | symbol, unknown> {     hidden?: boolean     alwaysShow?: boolean     title?: string     icon?: string     noCache?: boolean     breadcrumb?: boolean     affix?: boolean     activeMenu?: string     noTagsView?: boolean     followAuth?: string     canTo?: boolean   } }  type Component<T = any> =   | ReturnType<typeof defineComponent>   | (() => Promise<typeof import('*.vue')>)   | (() => Promise<T>)  declare global {   interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {     name: string     meta: RouteMeta     component?: Component | string     children?: AppRouteRecordRaw[]     props?: Recordable     fullPath?: string     keepAlive?: boolean   }    interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {     icon: any     name: string     meta: RouteMeta     component: string     componentName?: string     path: string     redirect: string     children?: AppCustomRouteRecordRaw[]     keepAlive?: boolean     visible?: boolean     parentId?: number     alwaysShow?: boolean   } }

接下来编写,创建路由、导出路由

 import type { App } from 'vue' import type { RouteRecordRaw } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router' import remainingRouter from './modules/remaining'  // 创建路由实例 const router = createRouter({   history: createWebHashHistory(), // createWebHashHistory URL带#,createWebHistory URL不带#   strict: true,   routes: remainingRouter as RouteRecordRaw[],   scrollBehavior: () => ({ left: 0, top: 0 }) })  // 导出路由实例 export const setupRouter = (app: App<Element>) => {   app.use(router) }  export default router

main.ts 中导入下

 import { createApp } from 'vue' import App from './App.vue' import { setupRouter } from './router/index' // 路由 import ElementPlus from 'element-plus' import 'element-plus/dist/index.css'  // 创建实例 const setupAll = async () => {   const app = createApp(App)   setupRouter(app)   app.mount('#app') }  setupAll()

接下来写下 Layout 架构

我们要实现的效果,是一个后台管理页面的侧边栏,点击菜单右边就能跳转到对应路由所在页面

图片

创建

AppMain.vue 右边路由跳转页

Sidebar.vue 侧边栏

index.vue 作为 layout 架构的统一出口

图片

 <!-- @description: AppMain -->  <template>   <div>     <router-view v-slot="{ Component, route }">       <transition name="fade-transform" mode="out-in"> <!-- 设置过渡动画 -->         <keep-alive>           <component :is="Component" :key="route.fullPath" />         </keep-alive>       </transition>     </router-view>   </div> </template>

上面是一种动态路由的固定写法,需要与的路由配置进行对应

其中最主要的就是 <component :is="Component" :key="route.fullPath" /> 中的 key,这是为确定路由跳转对应页面的标识,没这个就跳不了

有一个小知识点

  • route.fullPath 拿到的地址是包括 searchhash 在内的完整地址。该字符串是经过百分号编码的

  • route.path 经过百分号编码的 URL 中的 pathname

 //路径:http://127.0.0.1:3000/user?id=1 console.log(route.path) // 输出 /user console.log(route.fullPath) // 输出 /user?id=1

为了实现右边侧边栏,需要引入 element plus 来快速搭建

     pnpm install element-plus

main.ts 改造一下,完整引入 element-plus

 import { createApp } from 'vue' import App from './App.vue' import ElementPlus from 'element-plus' // element-plus 组件库 import 'element-plus/dist/index.css' // element-plus 组件库样式文件  // 创建实例 const setupAll = async () => {   const app = createApp(App)   app.use(ElementPlus)   app.mount('#app') }  setupAll()

我们来编写下 侧边栏

 <!-- @description: Sidebar -->  <template>   <div>     <el-menu active-text-color="#ffd04b" background-color="#304156" default-active="2" text-color="#fff" router>       <el-sub-menu :index="item.path" v-for="item in routers">         <template #title>{{ item.name }}</template>         <el-menu-item :index="child.path" v-for="child in item.children">{{ child.name }}</el-menu-item>       </el-sub-menu>     </el-menu>   </div> </template>  <script setup lang='ts'> import { filterRoutes } from '@/utils/router'; import { computed } from 'vue'; import { useRouter } from 'vue-router'; const router = useRouter() // 通过计算属性,路由发生变化时更新路由信息 const routers = computed(() => {   return filterRoutes(router.getRoutes()) // router.getRoutes() 用于获取路由信息 }) </script>

统一导出 layout 架构,加一点小样式

 <!-- @description: layout index -->  <template>   <div class="app-wrapper">     <Sidebar class="sidebar-container" />     <App-Main class="main-container" />   </div> </template>  <script setup lang='ts'> import { ref, reactive } from 'vue' import Sidebar from './components/Sidebar.vue' import AppMain from './components/AppMain.vue' </script>  <style scoped> .app-wrapper {     display: flex; } .sidebar-container {     width: 200px;     height: 100vh;     background-color: #304156;     color: #fff; } .main-container {     flex: 1;     height: 100vh;     background-color: #f0f2f5; } </style>

pnpm run serve 运行一下

图片

页面权限管理

通常我们实现页面权限管理,比较常见的方案是,有权限的路由信息由后端传给前端,前端再根据路由信息进行渲染

我们先安装下 pinia 模拟下后端传过来的数据

pnpm install pinia

图片

 import { defineStore } from "pinia";  interface AuthStore {   // 菜单   menus: any[]; }  export const useAuthStore = defineStore("authState", {   state: (): AuthStore => ({     menus: [       {         path: "/routing",         component: null,         redirect: "user",         children: [           {             path: "/routing/user",             component: "/user.vue",             name: "用户管理",             meta: {},           },           {             path: "/routing/menu",             component: "/menu.vue",             name: "菜单管理",             meta: {},           }         ],         name: "系统管理",         meta: undefined,       },     ]   }),   getters: {},   actions: {}, });

 好了,我们把模拟的路由数据,加到本地路由中

// permission.tsimport router from './router'import type { RouteRecordRaw } from 'vue-router'import { formatRoutes } from './utils/router'import { useAuthStore } from '@/store';import { App } from 'vue';

// 路由加载前router.beforeEach(async (to, from, next) => {  const { menus } = useAuthStore()  routerList.forEach((route) => {    router.addRoute(menus as unknown as RouteRecordRaw) // 动态添加可访问路由表  })  next()})
// 路由跳转之后调用router.afterEach((to) => { })

报错了,为什么呢?

对比路由表的数据,原来,组件模块的数据与公共路由的数据不一致

图片

我们需要把模拟后端传过来的数据处理一下

 // router.ts import Layout from '@/layout/index.vue'; import type { RouteRecordRaw } from 'vue-router'  /* 处理从后端传过来的路由数据 */ export const formatRoutes = (routes: any[]) => {   const formatedRoutes: RouteRecordRaw[] = []   routes.forEach(route => {       formatedRoutes.push(         {           ...route,           component: Layout, // 主要是将这个 null -> 组件           children: route.children.map((child: any) => {             return {               ...child,               component: () => import(`@/views${child.component}`), // 根据 本地路径配置页面路径             }           }),         }       )   })   return formatedRoutes; }

再修改下 permission.ts​​​​​​​​​​​​​​

 import router from './router' import type { RouteRecordRaw } from 'vue-router' import { formatRoutes } from './utils/router' import { useAuthStore } from '@/store'; import { App } from 'vue';   // 路由加载前 router.beforeEach(async (to, from, next) => {   const { menus } = useAuthStore()   const routerList = menus   routerList.forEach((route) => {     router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表   })   next() })  // 路由跳转之后调用 router.afterEach((to) => { })

main.ts 引入一下

 import './permission'

可以正常访问了

图片

按钮权限

除了页面权限,外我们还有按钮权限

可以通过自定义指令来完成,permission.ts 中定义一下

 /* 按钮权限 */ export function hasPermi(app: App<Element>) {   app.directive('hasPermi', (el, binding) => {     const { permissions } = useAuthStore()     const { value } = binding     const all_permission = '*:*:*'      if (value && value instanceof Array && value.length > 0) {       const permissionFlag = value        const hasPermissions = permissions.some((permission: string) => {         return all_permission === permission || permissionFlag.includes(permission)       })        if (!hasPermissions) {         el.parentNode && el.parentNode.removeChild(el)       }     } else {       throw new Error('权限不存在')     }   }) }  export const setupAuth = (app: App<Element>) => {   hasPermi(app) }

 需要挂载到 main.ts​​​​​​​

 import { createApp } from 'vue' import App from './App.vue' import { setupRouter } from './router/index' import ElementPlus from 'element-plus' import { createPinia } from 'pinia' import { setupAuth } from './permission' import 'element-plus/dist/index.css' import './permission'  // 创建实例 const setupAll = async () => {   const app = createApp(App)   setupRouter(app)   setupAuth(app)   app.use(ElementPlus)   app.use(createPinia())   app.mount('#app') }  setupAll()

还是在 store 那里加一下模拟数据

​​​​​​​​​​​​​​​​​​​​​

 export const useAuthStore = defineStore("authState", {   state: (): AuthStore => ({     menus: [       {         path: "/routing",         component: null,         redirect: "user",         children: [           {             path: "/routing/user",             component: "/user.vue",             name: "用户管理",             meta: {},           },           {             path: "/routing/menu",             component: "/menu.vue",             name: "菜单管理",             meta: {},           }         ],         name: "系统管理",         meta: undefined,       },     ],     permissions: [       // '*:*:*', // 所有权限       'system:user:create',       'system:user:update',       'system:user:delete',     ]   }), });

 user.vue加入几个按钮,使用自定义指令​​​​​​​

 <!-- user.vue --> <template>   <div>     <el-button type="primary" v-hasPermi="['system:user:create']">创建</el-button>     <el-button type="primary" v-hasPermi="['system:user:update']">更新</el-button>     <el-button type="primary" v-hasPermi="['system:user:delete']">删除</el-button>     <el-button type="primary" v-hasPermi="['system:user:admin']">没权限</el-button>   </div> </template>

system:user:admin 这个权限没有配置,无法显示

图片

加一下权限

图片

图片

扩展

用户权限我们使用 v-hasPermi自定义指令,其原理是通过删除当前元素,来实现隐藏

如果使用 Element Plus 的标签页呢

我们在 src/views/home.vue 写一下基本样式​​​​​​​

<!--@description: 主页--><template>  <div>    <el-tabs>      <el-tab-pane label="标签一" name="first">标签一</el-tab-pane>      <el-tab-pane label="标签二" name="second">标签二</el-tab-pane>    </el-tabs>  </div></template>
​​​​​​​

图片

我们加下按钮权限控制​​​​​​​

<template>  <div>    <el-tabs v-model="activeName">      <el-tab-pane label="标签一" v-hasPermi="['system:tabs:first']" name="first">标签一</el-tab-pane>      <el-tab-pane label="标签二" name="second">标签二</el-tab-pane>    </el-tabs>  </div></template>

图片

因为这个权限我们没有配置,标签页内容隐藏了,这没问题

但是,标签没隐藏啊,通常要是标签一没权限,应该是标签项、和标签内容都隐藏才对

为什么会这样呢?

我们在 hasPermi 自定义指令中,打印下获取到的元素

图片

图片

id 为 pane-firstpane-second 元素对应位置在哪里,我们找一下 需要先把指令去掉,因为元素都被我们删除的话,我们看不到具体DOM结构

图片

图片

图片

对比一下,明显可以看出 hasPermi 自定义指令获取到只是标签内容的元素

那怎么办?

解决办法一:根据当前元素,一层层找到标签项,然后删除,这样是可以。但是这样太麻烦了,也只能用于标签页,那要是其他组件有这样的问题咋办

解决办法二:我们写一个函数判断权限是否存在,再通过 v-if 进行隐藏

图片

 

 ​​​​​​

export function checkPermi(value: string[]) {  const { permissions } = useAuthStore()  const all_permission = '*:*:*'
  if (value && value instanceof Array && value.length > 0) {    const permissionFlag = value
    const hasPermissions = permissions.some((permission: string) => {      return all_permission === permission || permissionFlag.includes(permission)    })
    if (!hasPermissions) {      return false    }    return true  }}

src/views/home.vue,引入下 checkPermi​​​​​​​

<!--@description: 主页-->
<template>  <div>    <el-tabs v-model="activeName">      <el-tab-pane label="标签一" v-if="checkPermi(['system:tabs:first'])" name="first">标签一</el-tab-pane>      <el-tab-pane label="标签二" name="second">标签二</el-tab-pane>    </el-tabs>  </div></template>
<script setup lang='ts'>/* ------------------------ 导入 与 引用 ----------------------------------- */import { ref } from 'vue'import { checkPermi } from '@/permission';/* ------------------------ 变量 与 数据 ----------------------------------- */const activeName = ref('first')</script>

小结

页面权限

不同用户,具有不同页面访问权限,对应权限的路由信息由后端返回。 本地路由 + 后端传过来的路由 = 菜单路由

按钮权限

根据不同用户,后端传过来每个按钮的按钮权限字符串,前端根据自定义指令,判断该按钮权限字符串是否存在 从而显示或者隐藏

扩展

一些特殊情况下,自定义指令隐藏无法满足我们想要的效果,我们可以定义一个公共函数检测权限是否存在,再通过 v-if 进行隐藏

<think>好的,我现在需要回答用户关于RBAC在前端实现或应用的问题。首先,我需要回忆一下RBAC的基本概念。RBAC是基于角色的权限控制,通过角色来管理用户的访问权限。用户属于某个角色,角色拥有特定的权限,这样权限分配就更灵活了。 用户的问题集中在“前端实现方案”,所以我应该重点放在前端如何应用RBAC,比如路由控制、按钮权限、菜单显示等方面。根据提供的引用,特别是引用[4]和[5],里面提到了Vue中使用vuex和动态路由来实现RBAC。我需要结合这些信息来构建回答步骤。 首先,用户登录后需要获取权限数据,这可能是一个API请求返回用户的角色和权限列表。然后,前端需要根据这些权限动态生成可访问的路由。Vue Router允许动态添加路由,所以可以使用router.addRoute方法。同时,左侧菜单需要根据权限过滤显示,这可以通过vuex存储处理后的路由数据,然后在组件中读取并渲染。 另外,按钮级别的权限控制可以使用自定义指令,比如v-permission,根据权限标识显示或隐藏按钮。需要注意的是,前端权限控制只是辅助,后端也必须进行校验,防止绕过前端直接调用API。 可能还需要考虑路由的拆分,将公共路由和权限路由分开处理。引用[4]提到了动态拆分路由表和过滤,这需要将路由配置分为基础路由和动态路由,然后根据权限筛选出可访问的动态路由,再添加到路由器中。 在存储方面,vuex用来管理路由数据和权限状态,用户登录后触发action获取权限数据,处理路由,并提交mutation更新状态。同时,保持路由持久化可能需要结合本地存储,防止刷新后丢失。 最后,生成相关问题时要考虑用户可能的后续疑问,比如动态路由的具体实现、按钮权限、数据持久化、前后端协作等。确保问题覆盖用户可能想深入了解的方面。 现在需要把这些步骤整理成一个结构清晰的中文回答,引用相关的引用来源,并按照系统指令正确使用LaTeX格式。同时检查是否有遗漏点,比如前端存储处理后的路由,或如何处理路由的添加和菜单渲染。确保每个步骤都有对应的实现方法,并引用正确的来源。</think>### RBAC 前端实现方案分步说明 #### 1. **权限数据结构设计** 前端需先定义权限数据结构,通常包含角色标识、可访问路由列表、操作权限点(如按钮权限)。例如: ```json { "role": "admin", "routes": ["/dashboard", "/user/list"], "permissions": ["user:delete", "order:edit"] } ``` 此结构通过接口获取,需与后端约定字段名称[^5]。 #### 2. **动态路由控制** - **拆分路由表**:将路由分为`基础路由`(如登录页)和`动态路由`(需权限控制的路由)。 ```javascript // 基础路由(无需权限) const baseRoutes = [{ path: '/login', component: Login }]; // 动态路由(需权限) const dynamicRoutes = [ { path: '/dashboard', meta: { requireAuth: true, permission: 'dashboard' } } ]; ``` - **过滤路由**:根据接口返回的权限标识过滤动态路由: ```javascript // 过滤函数示例 function filterRoutes(userPermissions, dynamicRoutes) { return dynamicRoutes.filter(route => userPermissions.includes(route.meta.permission) ); } ``` - **添加动态路由**:使用`router.addRoute()`注入权限路由: ```javascript filteredRoutes.forEach(route => router.addRoute(route)); ``` 实现动态路由更新[^4]。 #### 3. **菜单渲染控制** 将过滤后的路由存入Vuex,供侧边栏组件读取: ```javascript // Vuex mutation示例 mutations: { SET_MENU_ROUTES(state, routes) { state.menuRoutes = routes; } } ``` 侧边栏组件通过`computed`获取路由数据并渲染: ```vue <template> <div v-for="route in menuRoutes" :key="route.path"> {{ route.meta.title }} </div> </template> <script> export default { computed: { menuRoutes() { return this.$store.state.menuRoutes; } } } </script> ``` 此方法实现菜单动态化[^4][^5]。 #### 4. **按钮级权限控制** 使用自定义指令`v-permission`控制按钮显示: ```javascript // 全局指令注册 Vue.directive('permission', { inserted(el, binding, vnode) { const { value } = binding; const permissions = store.state.user.permissions; if (!permissions.includes(value)) { el.parentNode.removeChild(el); } } }); ``` 使用方式: ```vue <button v-permission="'user:delete'">删除用户</button> ``` 未拥有权限时自动移除元素[^3]。 #### 5. **状态持久化** 通过`localStorage`或`sessionStorage`保存权限数据,避免刷新丢失: ```javascript // 登录成功后存储数据 localStorage.setItem('userPermissions', JSON.stringify(permissions)); // 初始化时读取 const savedPermissions = JSON.parse(localStorage.getItem('userPermissions')); ``` 需在路由守卫中校验状态有效性。 #### 6. **路由守卫校验** 在全局前置守卫中验证权限: ```javascript router.beforeEach((to, from, next) => { if (to.meta.requireAuth && !store.state.user.role) { next('/login'); } else { next(); } }); ``` 确保未授权页面无法访问。 #### 7. **与后端协作要点** - 接口需返回当前用户的`角色`、`路由权限标识`、`操作权限点` - 敏感操作(如删除)需后端二次验证权限 - 动态路由建议由后端返回完整路由结构,前端直接注册(需防范XSS攻击) --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值