前端如何实现权限控制?看这一篇就够了
ztj100 2024-12-29 07:22 22 浏览 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 拿到的地址是包括 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运行一下
添加图片注释,不超过 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-first、pane-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 进行隐藏
相关推荐
- 人生苦短,我要在VSCode里面用Python
-
轻沉发自浅度寺量子位出品|公众号QbitAI在程序员圈子里,VisualStudioCode(以下简称VSCode)可以说是目前最火的代码编辑器之一了。它是微软出品的一款可扩展的轻量...
- 亲测可用:Pycharm2019.3专业版永久激活教程
-
概述随着2020年的到来,又有一批Pycharm的激活码到期了,各位同仁估计也是在到处搜索激活方案,在这里,笔者为大家收录了一个永久激活的方案,亲测可用,欢迎下载尝试:免责声明本项目只做个人学习研究之...
- Python新手入门很简单(python教程入门)
-
我之前学习python走过很多的歧途,自学永远都是瞎猫碰死耗子一样,毫无头绪。后来心里一直都有一个做头条知识分享的梦,希望自己能够帮助曾经类似自己的人,于是我来了,每天更新5篇Python文章,喜欢的...
- Pycharm的设置和基本使用(pycharm运行设置)
-
这篇文章,主要是针对刚开始学习python语言,不怎么会使用pycharm的童鞋们;我来带领大家详细了解下pycharm页面及常用的一些功能,让大家能通过此篇文章能快速的开始编写python代码。一...
- 依旧是25年最拔尖的PyTorch实用教程!堪比付费级内容!
-
我真的想知道作者到底咋把PyTorch教程整得这么牛的啊?明明在内容上已经足以成为付费教材了,但作者偏要免费开源给大家学习!...
- 手把手教你 在Pytorch框架上部署和测试关键点人脸检测项目DBFace
-
这期教向大家介绍仅仅1.3M的轻量级高精度的关键点人脸检测模型DBFace,并手把手教你如何在自己的电脑端进行部署和测试运行,运行时bug解决。01.前言前段时间DBFace人脸检测库横空出世,...
- 进入Python的世界02外篇-Pycharm配置Pyqt6
-
为什么这样配置,要开发带UI的python也只能这样了,安装过程如下:一安装工具打开终端:pipinstallPyQt6PyQt6-tools二打开设置并汉化点击plugin,安装汉化插件,...
- vs code如何配置使用Anaconda(vscode调用anaconda库)
-
上一篇文章中(Anaconda使用完全指南),我们能介绍了Anaconda的安装和使用,以及如何在pycharm中配置Anaconda。本篇,将继续介绍在vscode中配置conda...
- pycharm中conda解释器无法配置(pycharm配置anaconda解释器)
-
之前用的好好的pycharm正常配置解释器突然不能用了?可以显示有这个环境然后确认后可以conda正在配置解释器,但是进度条结束后还是不成功!!试过了pycharm重启,pycharm重装,anaco...
- Volta:跨平台开发者的福音,统一前端js工具链从未如此简单!
-
我们都知道现在已经进入了Rust时代,不仅很多终端常用的工具都被rust重写了,而且现在很多前端工具也开始被Rust接手了,这不,现在就出现了一款JS工具管理工具,有了它,你可以管理多版本的js工具,...
- 开发者的福音,ElectronEgg: 新一代桌面应用开发框架
-
今天给大家介绍一个开源项目electron-egg。如果你是一个JS的前端开发人员,以前面对这项任务桌面应用开发在时,可能会感到无从下手,甚至觉得这是一项困难的挑战。ElectronEgg的出现,它能...
- 超强经得起考验的低代码开发平台Frappe
-
#挑战30天在头条写日记#开始进行管理软件的开发来讲,如果从头做起不是不可以,但选择一款免费的且经得起时间考验的低代码开发平台是非常有必要的,将大幅提升代码的质量、加快开发的效率、以及提高程序的扩展性...
- 一文带你搞懂Vue3 底层源码(vue3核心源码解析)
-
作者:妹红大大转发链接:https://mp.weixin.qq.com/s/D_PRIMAD6i225Pn-a_lzPA前言vue3出来有一段时间了。今天正式开始记录一下梗vue3.0.0-be...
- 基于小程序 DSL(微信、支付宝)的,可扩展的多端研发框架
-
Mor(发音为/mr/,类似more),是饿了么开发的一款基于小程序DSL的,可扩展的多端研发框架,使用小程序原生DSL构建,使用者只需书写一套(微信或支付宝)小程序,就可以通过Mor...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 人生苦短,我要在VSCode里面用Python
- 亲测可用:Pycharm2019.3专业版永久激活教程
- Python新手入门很简单(python教程入门)
- Pycharm的设置和基本使用(pycharm运行设置)
- 依旧是25年最拔尖的PyTorch实用教程!堪比付费级内容!
- 手把手教你 在Pytorch框架上部署和测试关键点人脸检测项目DBFace
- 进入Python的世界02外篇-Pycharm配置Pyqt6
- vs code如何配置使用Anaconda(vscode调用anaconda库)
- pycharm中conda解释器无法配置(pycharm配置anaconda解释器)
- Volta:跨平台开发者的福音,统一前端js工具链从未如此简单!
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)