王柳 发表于 2024-11-30 11:50:46

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



基本概念

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


[*] 基于 ACL 的权限控制
[*] 基于 RBAC 的权限控制
这个两种到底有什么不同呢?
我们通过下图来分析一下
https://img-blog.csdnimg.cn/img_convert/de4bfe73906267e1d6553f89cf8d5a5a.png
ACL 是基于 用户 -> 权限,直接为每个用户分配权限
RBAC 基于 用户 -> 脚色 -> 权限,以脚色为媒介,来为每个用户分配权限
这样做的好处是,某个权限过于敏感时,想要将每个用户或者部门用户的权限去掉,就不须要每个用户的权限都操纵一遍,只须要删除对应脚色的权限即可
那在实际的开辟中 RBAC 是最常用的权限控制方案,就前端而言,RBAC 主要如何实现的呢?
主要就两个部门


[*] 页面权限受控
[*] 按钮权限受控
下面我们就来实现这两个部门


[*] 页面权限
[*] 按钮权限
页面的访问,我们都是须要设置路由表的,根据设置路由表的路径来访问页面
那么,我们控制了路由表,不就能控制页面的访问了吗?
实现思绪


[*] 前端根据不同用户信息,从后端获取该用户所拥有权限的路由表
[*] 前端动态创建路由表
基本环境:
https://i-blog.csdnimg.cn/direct/46dac96ade8d4e5ab8918a3695f6e5b8.png
创建项目
 npm install -g @vue/cli
 vue --version # @vue/cli 5.0.8
 vue create vue-router-dome
https://img-blog.csdnimg.cn/img_convert/0e783d742d73540760a7730b53b15149.png
打开项目,npm run serve 运行一下
https://i-blog.csdnimg.cn/direct/2cdf429eda6c48acaf3568657448618c.png
代码初始化,删除不须要的一些文件
https://img-blog.csdnimg.cn/img_convert/987a4b8d19210343ded078df19857773.png
我们创建几个新文件夹
https://img-blog.csdnimg.cn/img_convert/1dfefd93f377ea278cf9d3fc4f47f98e.png
写下基本的页面
https://img-blog.csdnimg.cn/img_convert/15b1e67f05f0763ca2d92a72861ff9b2.png
 ​​​​​​
<!-- home.vue --><template><div>主页</div></template> <!-- menu.vue --> <template>   <div>菜单管理</div> </template> <!-- user.vue --> <template>   <div>用户管理</div> </template> 写下路由设置
https://img-blog.csdnimg.cn/img_convert/21e8659ee441be46d3ae5473ddb5e42f.png

  // 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 的范例
https://i-blog.csdnimg.cn/direct/6acef9cab61e4e0597abf6d4bbe78af5.png
// 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 架构
我们要实现的结果,是一个背景管理页面的侧边栏,点击菜单右边就能跳转到对应路由所在页面
https://img-blog.csdnimg.cn/img_convert/cb1ad2dd9b7a33c1f4e7f849d9799264.png
创建
AppMain.vue 右边路由跳转页
Sidebar.vue 侧边栏
index.vue 作为 layout 架构的统一出口
https://img-blog.csdnimg.cn/img_convert/775e58e161502d9968cab545831f8a12.png
<!-- @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 拿到的地点是包括 search 和 hash 在内的完备地点。该字符串是经过百分号编码的
[*] 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 运行一下
https://img-blog.csdnimg.cn/img_convert/8bfbf941f8ce899766f5363c70ba69e8.png

页面权限管理

通常我们实现页面权限管理,比力常见的方案是,有权限的路由信息由后端传给前端,前端再根据路由信息举行渲染
我们先安装下 pinia 模拟下后端传过来的数据


[*]
pnpm install pinia https://img-blog.csdnimg.cn/img_convert/68ce0d7d20a2652ff363ff06f51e22da.png


[*]
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: {}, });  好了,我们把模拟的路由数据,加到本地路由中
https://i-blog.csdnimg.cn/direct/220d9f428cf443249f6b30d4c5f9e042.png
// 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) => { }) https://i-blog.csdnimg.cn/direct/efb7dd280882453da493a9c29d643185.png
https://i-blog.csdnimg.cn/direct/79acf4ea7049402a829bcf1f82963a23.png
报错了,为什么呢?
对比路由表的数据,原来,组件模块的数据与公共路由的数据不一致
https://img-blog.csdnimg.cn/img_convert/880a8762a3e05b65aa40241f2f8f3c70.png
我们须要把模拟后端传过来的数据处理惩罚一下
https://i-blog.csdnimg.cn/direct/39e2db65db9c46df9660ff977a93cc99.png

// 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'
可以正常访问了
https://img-blog.csdnimg.cn/img_convert/b03b45d4fff8b12395b3ce19e0315f96.png
按钮权限

除了页面权限,外我们还有按钮权限
可以通过自界说指令来完成,permission.ts 中界说一下
https://i-blog.csdnimg.cn/direct/5069734fdf8f4391b7c1853e37e94580.png
/* 按钮权限 */ 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 这个权限没有设置,无法显示
https://img-blog.csdnimg.cn/img_convert/6719a35996ac681a619afb633061b071.png
加一下权限
https://img-blog.csdnimg.cn/img_convert/2a6dfa06b37d0534a5194c8cacc7f1f3.png
https://img-blog.csdnimg.cn/img_convert/18251c9fcb05660f8159372792c2ff9e.png
扩展

用户权限我们使用 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>
​​​​​​​ https://img-blog.csdnimg.cn/img_convert/4da0243c6c0b9e6233202b84aa7fe28a.png
我们加下按钮权限控制​​​​​​​
<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>
https://img-blog.csdnimg.cn/img_convert/1c2c2b500e4e34eeb77bf5fee8cbe5c1.png


因为这个权限我们没有设置,标签页内包庇藏了,这没问题
但是,标签没隐藏啊,通常要是标签一没权限,应该是标签项、和标签内容都隐藏才对
为什么会这样呢?
我们在 hasPermi 自界说指令中,打印下获取到的元素

https://img-blog.csdnimg.cn/img_convert/c437c577af5b5063fc24120942fbf827.png
https://img-blog.csdnimg.cn/img_convert/84d3685a4ea4b9be94b211ce077bb31d.png
id 为 pane-first、pane-second 元素对应位置在那边,我们找一下 须要先把指令去掉,因为元素都被我们删除的话,我们看不到详细DOM布局
https://img-blog.csdnimg.cn/img_convert/96c34516b219020db968ca38620a23fa.png
https://img-blog.csdnimg.cn/img_convert/b584d3f421e18c9a316a3d3240ce5ac2.png
https://img-blog.csdnimg.cn/img_convert/84d3685a4ea4b9be94b211ce077bb31d.png
对比一下,明显可以看出 hasPermi 自界说指令获取到只是标签内容的元素
那怎么办?
解决办法一:根据当前元素,一层层找到标签项,然后删除,这样是可以。但是这样太麻烦了,也只能用于标签页,那要是其他组件有这样的问题咋办
解决办法二:我们写一个函数判断权限是否存在,再通过 v-if 举行隐藏
https://img-blog.csdnimg.cn/img_convert/a9bce6fef1838c40a5c11d3758869261.png
    ​​​​​​
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> https://i-blog.csdnimg.cn/direct/4bebf2ed38404ee8beaded949d38a7ac.png
小结

页面权限
不同用户,具有不同页面访问权限,对应权限的路由信息由后端返回。 本地路由 + 后端传过来的路由 = 菜单路由
按钮权限
根据不同用户,后端传过来每个按钮的按钮权限字符串,前端根据自界说指令,判断该按钮权限字符串是否存在 从而显示或者隐藏
扩展
一些特殊环境下,自界说指令隐藏无法满足我们想要的结果,我们可以界说一个公共函数检测权限是否存在,再通过 v-if 举行隐藏


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 前端如何实现权限控制?看这一篇就够了