实现RBAC

一、菜单权限的控制

  • 左边侧边栏的菜单显示是来自于userlnfoStore.menuRoutes,当前进行打印,得到的内容其实就是staticRoutes静态路由表定义的路由数组对象;
  • 最终staticRoutes(静态路由)、allAsyncRoutes(动态路由)、anyRoute(任意路由)需要进行一次整合、拼接,最终合成一个路由数组对象。而这个路由对象的内容最终也会被放到menuRoutes菜单路由对象当中进行左边侧边栏的菜单显示操作。  
  • userInfoStore.menuRoutes: 只进行侧边栏菜单的显示
  • store/index.ts里的router的实例中的routes实现的是路由的真正跳转
  • 所以需要修改仓库中的静态、动态、任意路由,进行拼接处理
  • 创建filterAsyncRoutes递归函数,利用routes与allAsyncRoutes进行对比,产生授权以后的路由对象

? 问题,表现形式:
1.以trademark003进行登录,能够确认的是他有商品管理下的品牌管理的权限,children只有1个

2.退出trademark003登录,重新以admin进行登录,按道理说他有商品管理下的所有页面的操作权限,应该有5个children,但是现在却仍旧只有1个
? 原因: 因为我们对 alLAsyncRoutes 进行操作,操作的都是原来的数组对象,没有发生改变

? 解决: 对alAsyncRoutes进行深拷贝,然后再进行操作,这样,每次aLLAsyncRoutes都是一个副本,不再是原对象

  userInfo.ts文件

function filterAsyncRoutes(
    allAsyncRoutes: RouteRecordRaw[],
    routesName: string[]
) {
    // 拿右边比左边(拿allAsyncRoutes比routesName),因为核心算法是递归,右边是有递归的;左边没有递归
    return allAsyncRoutes.filter((route) => {
        //routesName是一维数组
        if (routesName.indexof(route.name as string) === -1) {
            // 没有在路由字符串中找到此路由名称
            return false;
        }
        // 找到后,继续判断 动态路由的子路由是否在路由字符串中存在
        if(route.children &&  route.children.length > 0) {
            //递归函数的特点: 函数调函数自身,需要有跳出机制,否则死循环
            route.children = filterAsyncRoutes(route.children,routesName)
        }
        return true;
    }
}        


// 获取用户信息是在/permission.ts授权路由守卫中使用
async getInfo() {
    const info = await getuserInfoApi();
    this.name =.info.name ;
    this.avatar= info.avatar;
    this.roles = info.roles;
    this.authBtnList = info.buttons;
    console.log(info.routes); // 一个一维数组,里面都是string字符串

    // 将动态路由进行了递归的条件判断,选择出符合权限的路由对象(调用方法,参数一:动态路由,参数二:接收后端返回的routes字符串数组)
    const _allAsyncRoutes = filterAsyncRoutes(cloneDeep(allAsyncRoutes),info.routes);  // cloneDeep: 深拷贝的方法

    // 路由的合并操作,利用动态路由操作
    //addRoutes的参数只包含动态路由和任意路由,并没有静态路由,但是我们想构建的路由其实是静态、动态、任意三者的合并路由
    addRoutes([...allAsyncRoutes,anyRoute]);

    //为什么不加anyRoute(因为路由是需要实现跳转的,菜单的显示仅仅是菜单的显示而已,虽然菜单的显示内容是从路由中来,但是菜单的显示内容并不是路由)
    this.menuRoutes = [...staticRoutes,..._allAsyncRoutes];
}
  • 需要利用路由中addRoute动态添加路由的功能对授权以后的异步路由内容进行循环遍历,以便动态的将这些路由对象内容添加到当前已经存在的路由routes当中,这样的目的是为了可以进行URL当中的路由的跳转操作,但是不能够进行侧边栏菜单的显示
  • 侧边栏菜单的显示是由menuRoutes进行实现的,而它仅仅实现的是菜单的显示,不能够进行路由的跳转操作

  index.ts文件

import { createRouter, createWebHistory } from "vue-router";
import { staticRoutes } from "@/router/routes";

const router = createRouter({
    history: createwebHistory(),
    routes:staticRoutes,
    scrollBehavior() {
        return { top:0, left: 0};
    },
});

//导出路由
export default router;

  userInfo.ts文件

function addRoutes( routes:  RouteRecordRaw[]){
    // router是路由对象,而且router里本身已经有一个属性叫routes,并且routes的内容是静态路由
    // 说明router已经有了静态路由,所以我们只需要操作动态和异步
    routes.forEach((route) => {
        router.addRoute(route);
    });
}

二、菜单权限路由控制的总结

  • RBAC的层次结构: 用户N => 角色N => 资源N
  • 路由的分类: 静态、动态、任意
  • 动态权限路由: 想要实现addRoute动态注册路由的操作,首先得将后台返回的routes数组与异步路由进行递归的比较,得到的路由是授权以后的路由对象,然后对该数组对象进行循环遍历,利用addRoute实现动态路由的注册操作
  • 需要注意的细节是原路由对象和用户重新登录以后获取到的路由对象是同一对象,所以为了划分出不同的用户的路由对象,可以对动态生成的路由内容进行深拷贝操作。
  • 路由的跳转与菜单的显示是两个不的概念,上述只是实现了路由的跳转操作,但想要控制菜单的显示需要设置的是menuRoutes,所以只需要将静态路由与动态路由(授权控制以后能够操作的动态路由)进行合并即可,因为任意路由不需要显示在菜单上。
  • 光menuRoutes进行菜单的显示也是不行的,一定要利用动态路由进行路由跳转的控制,所以菜单显示与路由跳转是缺一不可的。

三、新增新的功能版块

  • 如果有一个新的功能模块,前端要不要进行工作的处理? 需要进行哪些工作的处理?
    • 在动态的异步路由中需要进行新的功能版块路由的设置操作,路径是对应到指定的视图页面的 ===> routes
    • 在以往的操作过程中,如果想要实现views视图层,首先得确认请求的二次封装、数据模型、接口的统一管理、公共组件的抽离、store仓库的定义、hook钩子的封装....
    • 需要加增异步路由中所对应的视图页面===> views
  • 准备进行一个ACL权限管理的新模块增加操作
    • 修改了router/routes.ts中的allAsyncRoutes异步动态路由,将ACL权限管理模块的内容进行设置操作
    • 下载并复制粘贴覆盖了api下面的数据模型和接口的统一管理内容
    • 下载并复制粘贴了views下面的视图页面
  • 如果一个新的功能板块添加,后台要不要进行工作的处理? 要进行哪些工作的处理?
    • 在前端进行了新版块路由以及接口和视图等工作的处理完毕以后,后台的权限管理的数据也需要进行一一对应。如果不对应,那么,我们将无法实现功能版块的权限设置操作,最终用户也无法查看到对应的菜单无法实现对应页面的路由跳转与显示
    • 当前我们进行的是菜单权限管理,所以后台的操作顺序应该是: 先进行菜单资源管理==> 角色授权==> 用户授权操作

四、权限管理页面的可实现性分析

  • 用户管理
    • 带分页的用户列表 (table、 button、pagination、插槽),生命周期钩子函数
    • 添加、修改用户 (dialog对话框,form表单,rules表单校验,button按钮)
    • 删除用户(popconfirm提示确认框)
    • 批量删除 (table里的column列的type为selection多选,@selection-change进行改变,获取到数组,数组里有对象),对selection选中内容进行map循环遍历,返回所有的id数组,可以考虑将其变成字符串。
    • 搜索功能(中转操作): usename和searchUsername两个内容 (思考,为什么需要有两个内容,中转操作),这样做的目的是为了防止在进行搜索的时候,输入框的值发生改变,导致请求的数据不正确
    • 分配角色: checkbox多选的v-model应用,在进行接口操作的时候需要传递选中的角色的id列表
  • 角色管理
    • 带分页的角色列表(table、button、pagination、插槽),生命周期钩子函数
    • 添加、修改角色 (dialog对话框,form表单,rules表单校验,button按钮)
    • 删除角色(popconfirm提示确认框)
    • 批量删除 (table里的column列的type为selection多选,@selection-change进行改变,获取到数组,数组里有对象),对selection选中内容进行map循环遍历,返回所有的id数组,可以考虑将其变成字符串。
    • 搜索功能(中转操作): roleName和searchRoleName两个内容(思考,为什么需要有两个内容,中转操作),这样做的目的是为了防止在进行搜索的时候,输入框的值发生改变,导致请求的数据不正确
    • 分配权限:
      • 显示的是一个tree树形组件,而树形组件中有半选状态,有全选状态,在进行权限选择的时候,最后进行数据提交的时候,需要将所有的半选、全选全部都选中,这样才是合理的。不能只选全选的状态内容。所以需要将半选与全选的内容进行拼接操作作。
      • 树形组件的数据结构是类似于路由的children的嵌套结构,所以需要利用的算法是递归算法
  • 菜单管理
    • table表格的展开模式实现树形的界面效果
    • 表格不带分页,是展开式操作,button、slot、dialog、form、rules

五、按钮级权限管理

  • vue2当中的自定义指令有生命周期钩子函数,比如bind、inserted、 update、componentUpdate、 unbind,里面有el、binding、vNode、 oldVNode等参数。
  • vue3当中自定义指令的生命周期已经完全发生了变化,created/beforeMount/mounted/beforeUpdate/updated/beforeUnmount/unmounted这些生命周期钩子函数,看起来除了beforeCreate没有了,其它的生命周期钩子函数与页面的生命周期钩子函数是保持一致的。而钩子函数中的参数也有(el, binding,vnode, prevVnode)
  • 可以创建一个directives的目录,并且新建一个has.ts的程序文件,该程序文件中虽然想实现指令的构建,但是这个指令将被创建于插件当中。
  • 只需要在指令中利用条件判断,去判断字符串有没有在对应的数组当中,那么就可以考虑是否将DOM元素进行删除。
  • 因为是插件,插件中有app.directive全局指令的构建,所以任何页面都可以使用v-has进行自定义指令的调用,当前操作的前提是在入口文件中将插件引入并且进行use(has)的使用。

  has.ts文件

//从store仓库中获取按钮的数组列表
import pinia from "@/stores";
import { useUserInfoStore ]from "@/stores/userInfo";
import type { App } from "vue";
//我们当前并不在组件中,所以没办法使用原型链的方式来挂载,所以我们使用插件的方式来挂载
const userInfoStore= useUserInfoStore(pinia);

// 进行一个自定义插件的定义
export default (app: App) => {
    // 自定义插件当中可以做的事情有很多,比如全局组件、全局指令、全局方法、实例方法(vue2)
    //全局指令
    app.directive("has", {
        mounted(el, binding){
            if (userInfoStore.authBtnList.indexOf(binding.value) === -1) {
                // 没有找到匹配的内容,所以元素不应该显示,应该删除
                el.parentNode && el.parentNode.removeChild(el);
            }
        },
    });
};
    

  main.ts文件

import { createApp } from "vue";
import pinia from."./stores";
import ElementPlus from "element-plus";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import "element-plus/dist/index.css";
import App from "./App.vue";
import router from "./router";
import "./styles/index.scss";
import ElSvg from "./components/SvgIcon/ElSvg";
import "./permission";
import CategorySelector from "@/components/CategorySelector/index.vue";
import has from "@/directive/has";

const app = createApp(App);

app.component(CategorySelector.name,CategorySelector);

ElSvg(app);

app
  .use(pinia)
  .use(has)
  .use(router)
  .use(ElementPlus, {
    locale: zhCn,
  })
  .mount("#app");

  index.vue页面使用

<el-button
    size="small"
    @click="$event = showUpdateDialog(scope.row)"
    v-has="'btn.Trademark.update'"   //自定义指令使用,判断修改按钮是否正常显示
    >修改</el-button
>

六、路由守卫

  • 权限与路由守卫之间的关系是什么? 权限操作一定是会使用路由守卫功能的,因为只有在守卫的情况下内容才可以进行限制的访问。
  • 可能根据情况设置白名单与黑名单
  • 具体情况是根据需求判断token以便进行next的放行处理

  permission.ts

import router from "@/router";
import NProgress from "nprogress";  // 进度条
import "nprogress/nprogress.css"
import pinia from "@/stores";
import { useUserInfoStore } from "@/stores/userInfo";
import { ElMessage } from "element-plus";
import getPageTitle from "./utils/get-page-title";

NProgress.configure({ showSpinner: false });
const userInfoStore = useuserInfoStore(pinia);

// 不用进行token检查的白名单路径数组
const whiteList = ["/login"];

// 路由加载前
router.beforeEach(async (to, from, next) => {
    // 在显示进度条
    NProgress.start();

    // 设置整个页面的标题
    document.title = getPageTitle(to.meta.title as string);
    
    const token = userInfoStore.token;

    // 如果token存在(已经登陆或前面登陆过)
    if (token) {
        //如果请求的是登陆路由
        if (to.path === "/login") {
            //直接跳转到根路由,并完成进度条
            next({ path: "/" });
            NProgress.done();
        } else {
            // 请求的不是登陆路由
            // 是否已经登陆
            const hasLogin = !!userInfoStore.name;
            // 如果已经登陆直接放行
            if (hasLogin) {
                next();
            } else {
                // 如果还没有登陆
                try {
                    // 异步请求获取用户信息(包含权限数据) ===> 动态注册用户的权限路由 => 当次跳转不可见
                    await userInfoStore.getInfo( );
                    next(to); // 重新跳转去目标路由,能看到动态添加的异步路由,且不会丢失参数                           
                    NProgress.done(); // 结束进度条
                } catch (error:any) {
                    // 如果请求处理过程中出错
                    // 重置用户信息
                    await userInfostore.reset():
                    // 提示错误信息
                    // ELMessage.error(error.message || 'Has Error') // axios拦截器中已经有提示了
                    // 跳转到登陆页面,并携带原本要跳转的路由路径,用于登陆成功后跳转
                    next(`/login?redirect=${to.path}`);
                    //完成进度条
                    NProgress.done( );
                }
            }
        }
    } else {
        // 没有token
        // 如果目标路径在白名单中(是不需要token的路径)
        if (whiteList.indexOf(to.path) !== -1){
            // 放行
            next();
        } else {
            // 如果没在白名单中,跳转到登陆路由携带原目标路径
            next(`/login?redirect=${to.path}`);
            // 完成进度条  当次跳转中断了要进行个新的跳转了
            NProgress.done();
        }
    }
});

// 路由加载后
router.afterEach(() => {
    NProgress .done();
});

七、权限总结

  • 预设资源,菜单的资源有没有预设,按钮的资源有没有预设。菜单与按钮的资源是一一对应的,而每个菜单下的按钮是根据这个版块具体的业务流程进行确认的。
  • 角色管理
    • 普通角色在进行新增、修改、删除操作的时候都很简单
    • 但是角色有一个授权功能,也就是给这个角色进行资源权限的分配操作(菜单、按钮不同的资源),需要强化tree树形组件的应用能力
  • 用户管理
    • 用户在进行新增、修改、删除操作的时候也很简单,但是会多出一给用户进行角色分配的操作
    • 用户对于角色授权来讲是一对多的关系
    • 一个用户可以拥有多个角色
    • 一个角色也可以分配给不同的用户,所以最终用户与角色是多对多的关系
    • 所以用户在进行角色分配的时候使用的是checkbox多选框操作

八、面试的时候如何介绍权限

  • 我们之前项目是有比较完善的权限管理模块的(告诉对方的信息是我做过很多的项目,并且涉及权限管理模块,甚至负责过权限管理模块,因为它是基础模块,所以我的经验是比较丰富的)
  • 采用的模式是RBAC基于角色的权限控制(有专业的术语,让对方深入感受你对于模块的理解,明确你的专业)
  • 项目的权限管理主要包括三大核心版块: 主要包括用户管理、角色管理以及资源管理(相对具体的内容的介绍,表明我确实是处理过对应版块内容的)
  • 这三个版块的基础是资源管理,而资源管理相对是固定的,资源的新增、修改、删除等操作都会影响到整个项目的版本(更具体的描述,不进行代码介绍,强调的模块的业务流程)
  • 资源管理事实上主要针对的是菜单权限与按钮权限,因为菜单对应的就是页面,而按钮对应的是页面中具体按钮的可操作性,所以在进行菜单权限管理的时候对应的需要分配按钮的权限
  • 想要将用户与资源之间建立起关系,其实可以利用角色来进行桥接操作,所以只需要级用户进行一个角色的识别,确认用户拥有多个身份角色即可 (明确三层结构的定位)
  • 用户与角色之间是多对多的对应关系,所以在操作的过程中还是有一定的复杂度的。最终需要通过角色拥有的资源权限分配到用户身上以后产生并集,确认用户最终所拥有的权限
  • 用户在和角色以及资源进行权限明确的情况下,只需要一登录就可以明确他的身份,拥有的菜单与按钮的权限
  • 权限的菜单资源主要实现的是路由当中的addRoute动态注册的功能,涉及到基本的算法是递归,因为路由存在多层嵌套的层次
  • 想要确认一个模块中的按钮权限往往是比较简单的,只需要利用插件、指令、函数、钩子等代码的封装方式就可以,实现简单数组的条件判断,但是按钮的权限操作实现的是硬编码,所以需要与后端开发进行“识别码”的共同约定
  • 在进行权限操作时一定会配合的是路由守卫功能,可以设置白名单、黑名单等内容进行token以及权限的认证判断(我们对于路由守卫也比较了解)
  • RBAC权限管理它的重点和难点是在于后台开发,但是前端涉及的内容也非常的多,所以前后台人员沟通配合是一个关键性要素,而我对于权限管理的业务流程以及操作模式都是比较熟悉的,所以能够更好的与后端开发人员进行紧密的沟通 (协作能力)
  • 因为我们之前的项目还是属于中小型项目,所以权限管理的操作还限于菜单级、按钮级,至于数据级的权限控制并没有过多的涉及,不知道我们当前项目中权限的细度有没有强化到数据级(不光要自己说,还得别人说)
  • 20
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值