前端如何实现权限控制?看这一篇就够了
点击关注公众号,“技术干货”及时达!基本概念权限控制,最常见的基本上有 2 种
基于 ACL 的权限控制基于 RBAC 的权限控制这个两种到底有什么不同呢?
我们通过下图来分析一下
image-20240427221653200.pngACL 是基于 用户 - 权限,直接为每个用户分配权限
RBAC 基于 用户 - 角色 - 权限,以角色为媒介,来为每个用户分配权限
这样做的好处是,某个权限过于敏感时,想要将每个用户或者部分用户的权限去掉,就不需要每个用户的权限都操作一遍,只需要删除对应角色的权限即可
那在实际的开发中 RBAC 是最常用的权限控制方案,就前端而言,RBAC 主要如何实现的呢?
主要就两个部分
页面权限受控按钮权限受控下面我们就来实现这两个部分
页面权限按钮权限页面的访问,我们都是需要配置路由表的,根据配置路由表的路径来访问页面
那么,我们控制了路由表,不就能控制页面的访问了吗?
实现思路
前端根据不同用户信息,从后端获取该用户所拥有权限的路由表前端动态创建路由表基本环境
image-20240428095213291.png创建项目
npm install -g @vue/cli
vue --version # @vue/cli 5.0.8
vue create vue-router-dome
image-20240428100652805.png打开项目,npm run serve 运行一下
image-20240428101422341.png代码初始化,删除不必要的一些文件
image-20240430141733619.png我们创建几个新文件夹
image-20240430142101446.png写下基本的页面
image-20240430164545661.png!-- home.vue --template div/div/template!-- menu.vue -- template div菜单管理/div /template !-- user.vue -- template div用户管理/div /template写下路由配置
image-20240430150442282.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 remainingRouterremaining 主要为了存放一些公共路由,没有权限页可以访问,比如登录页、404页面这些
因为是用 typescript 编写的,我们需要加一下声明文件,定义下 remainingRouter 的类型
image-20240430150828363.png // router.d.ts import type { RouteRecordRaw } from 'vue-router' import { defineComponent } from 'vue' declare module 'vue-router' { interface RouteMeta extends Recordstring | 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 ComponentT = any = | ReturnTypetypeof defineComponent | (() = Promisetypeof import('*.vue')) | (() = Promise) declare global { interface AppRouteRecordRaw extends OmitRouteRecordRaw, 'meta' { name: string meta: RouteMeta component?: Component | string children?: AppRouteRecordRaw[] props?: Recordable fullPath?: string keepAlive?: boolean } interface AppCustomRouteRecordRaw extends OmitRouteRecordRaw, '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: AppElement) = { 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 架构
我们要实现的效果,是一个后台管理页面的侧边栏,点击菜单右边就能跳转到对应路由所在页面
image-20240430142444903.png创建
AppMain.vue 右边路由跳转页
Sidebar.vue 侧边栏
index.vue 作为 layout 架构的统一出口
image-20240430142547758.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-plusmain.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; } /stylepnpm run serve 运行一下
image-20240430151909980.png页面权限管理通常我们实现页面权限管理,比较常见的方案是,有权限的路由信息由后端传给前端,前端再根据路由信息进行渲染
我们先安装下 pinia 模拟下后端传过来的数据
pnpm install piniaimage-20240430160445750.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: {}, });好了,我们把模拟的路由数据,加到本地路由中
image-20240430163450010.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) = { })image-20240430163840687.pngimage-20240430163946542.png报错了,为什么呢?
对比路由表的数据,原来,组件模块的数据与公共路由的数据不一致
image-20240430160855268.png我们需要把模拟后端传过来的数据处理一下
image-20240430161212799.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'
可以正常访问了
image-20240430164159080.png按钮权限除了页面权限,外我们还有按钮权限
可以通过自定义指令来完成,permission.ts 中定义一下
image-20240430164944266.png /* 按钮权限 */ export function hasPermi(app: AppElement) { 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: AppElement) = { 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 /templatesystem:user:admin 这个权限没有配置,无法显示
image-20240430165542815.png加一下权限
image-20240430165757893.pngimage-20240430165736044.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
script setup lang='ts'
/scriptimage-20240601152747411.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/templateimage-20240601152918869.png因为这个权限我们没有配置,标签页内容隐藏了,这没问题
但是,标签没隐藏啊,通常要是标签一没权限,应该是标签项、和标签内容都隐藏才对
为什么会这样呢?
我们在 hasPermi 自定义指令中,打印下获取到的元素
id 为 pane-first、pane-second 元素对应位置在哪里,我们找一下
需要先把指令去掉,因为元素都被我们删除的话,我们看不到具体DOM结构
对比一下,明显可以看出 hasPermi 自定义指令获取到只是标签内容的元素
那怎么办?
解决办法一:根据当前元素,一层层找到标签项,然后删除,这样是可以。但是这样太麻烦了,也只能用于标签页,那要是其他组件有这样的问题咋办
解决办法二:我们写一个函数判断权限是否存在,再通过 v-if 进行隐藏
image-20240601155249647.pngexport 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')/scriptimage-20240601155438278.png小结页面权限
不同用户,具有不同页面访问权限,对应权限的路由信息由后端返回。
本地路由 + 后端传过来的路由 = 菜单路由
按钮权限
根据不同用户,后端传过来每个按钮的按钮权限字符串,前端根据自定义指令,判断该按钮权限字符串是否存在
从而显示或者隐藏
扩展
一些特殊情况下,自定义指令隐藏无法满足我们想要的效果,我们可以定义一个公共函数检测权限是否存在,再通过 v-if 进行隐藏
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线