百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

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

ztj100 2024-12-29 07:22 29 浏览 0 评论

基本概念

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

  • 基于 ACL 的权限控制
  • 基于 RBAC 的权限控制

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

我们通过下图来分析一下

添加图片注释,不超过 140 字(可选)

ACL 是基于 用户 -> 权限,直接为每个用户分配权限 RBAC 基于 用户 -> 角色 -> 权限,以角色为媒介,来为每个用户分配权限 这样做的好处是,某个权限过于敏感时,想要将每个用户或者部分用户的权限去掉,就不需要每个用户的权限都操作一遍,只需要删除对应角色的权限即可 那在实际的开发中 RBAC 是最常用的权限控制方案,就前端而言,RBAC 主要如何实现的呢? 主要就两个部分

  • 页面权限受控
  • 按钮权限受控

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

  • 页面权限
  • 按钮权限

页面的访问,我们都是需要配置路由表的,根据配置路由表的路径来访问页面 那么,我们控制了路由表,不就能控制页面的访问了吗? 实现思路

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

基本环境

创建项目

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

添加图片注释,不超过 140 字(可选)

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

添加图片注释,不超过 140 字(可选)

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

添加图片注释,不超过 140 字(可选)

我们创建几个新文件夹

添加图片注释,不超过 140 字(可选)

写下基本的页面

添加图片注释,不超过 140 字(可选)

<!-- home.vue -->
<template>
  <div>主页</div>
</template>


<!-- menu.vue -->
 <template>
   <div>菜单管理</div>
 </template>


 <!-- user.vue -->
 <template>
   <div>用户管理</div>
 </template>

写下路由配置

添加图片注释,不超过 140 字(可选)

 // 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 的类型

添加图片注释,不超过 140 字(可选)

 // 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 架构

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

添加图片注释,不超过 140 字(可选)

创建

AppMain.vue 右边路由跳转页

Sidebar.vue 侧边栏

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

添加图片注释,不超过 140 字(可选)

 <!--
 @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运行一下

添加图片注释,不超过 140 字(可选)

页面权限管理

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

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

pnpm install pinia

添加图片注释,不超过 140 字(可选)

 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: {},
 });

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

添加图片注释,不超过 140 字(可选)

// 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()
  routerList.forEach((route) => {
    router.addRoute(menus as unknown as RouteRecordRaw) // 动态添加可访问路由表
  })
  next()
})

// 路由跳转之后调用
router.afterEach((to) => { })

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

报错了,为什么呢?

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

添加图片注释,不超过 140 字(可选)

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

添加图片注释,不超过 140 字(可选)

 // 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'

可以正常访问了

添加图片注释,不超过 140 字(可选)

按钮权限

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

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

添加图片注释,不超过 140 字(可选)

 /* 按钮权限 */
 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这个权限没有配置,无法显示

添加图片注释,不超过 140 字(可选)

加一下权限

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

扩展

用户权限我们使用 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'>

</script>

添加图片注释,不超过 140 字(可选)

我们加下按钮权限控制

<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>

添加图片注释,不超过 140 字(可选)

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

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

为什么会这样呢?

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

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

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

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

对比一下,明显可以看出 hasPermi 自定义指令获取到只是标签内容的元素 那怎么办? 解决办法一:根据当前元素,一层层找到标签项,然后删除,这样是可以。但是这样太麻烦了,也只能用于标签页,那要是其他组件有这样的问题咋办 解决办法二:我们写一个函数判断权限是否存在,再通过 v-if 进行隐藏

添加图片注释,不超过 140 字(可选)

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> 

添加图片注释,不超过 140 字(可选)

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

相关推荐

sharding-jdbc实现`分库分表`与`读写分离`

一、前言本文将基于以下环境整合...

三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么

在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...

MySQL8行级锁_mysql如何加行级锁

MySQL8行级锁版本:8.0.34基本概念...

mysql使用小技巧_mysql使用入门

1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...

MySQL/MariaDB中如何支持全部的Unicode?

永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...

聊聊 MySQL Server 可执行注释,你懂了吗?

前言MySQLServer当前支持如下3种注释风格:...

MySQL系列-源码编译安装(v5.7.34)

一、系统环境要求...

MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了

对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...

MySQL字符问题_mysql中字符串的位置

中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...

深圳尚学堂:mysql基本sql语句大全(三)

数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...

MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?

大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...

一文讲清怎么利用Python Django实现Excel数据表的导入导出功能

摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...

用DataX实现两个MySQL实例间的数据同步

DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...

MySQL数据库知识_mysql数据库基础知识

MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...

如何为MySQL中的JSON字段设置索引

背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...

取消回复欢迎 发表评论: