0


【Vue】权限控制

权限管理

分类:

  1. 页面权限
  2. 功能(按钮)权限
  3. 接口权限

vue3-element-admin 的实现方案

一般我们在业务中将 路由可以分为两种,

constantRoutes

asyncRoutes

  • constantRoutes: 代表那些不需要动态判断权限的路由,如登录页、404(或者不存在的路由)、首页、数据大屏等通用页面。
  • asyncRoutes: 代表那些需求动态判断权限并通过 addRoutes 动态添加的页面。

后台管理系统中的路由都具有不同的访问权限,侧边菜单栏也是同理,需要根据权限,异步生成。

整体步骤都十分类似:

我们在登录后获取 token ,将其存入 localStorage 中,用来“象征用户身份”。

登录表单提交业务实现:

/** 登录表单提交 */functionhandleLoginSubmit(){
  loginFormRef.value?.validate((valid: boolean)=>{if(valid){
      loading.value =true;
      userStore
        .login(loginData.value).then(()=>{const{ path, queryParams }=parseRedirect();
          router.push({path: path,query: queryParams });}).catch(()=>{getCaptcha();}).finally(()=>{
          loading.value =false;});}});}

调用登录接口,存储 token 到localStorage 中。

/**
 * 登录
 * @param {LoginData}
 * @returns
 */functionlogin(loginData: LoginData){returnnewPromise<void>((resolve, reject)=>{
    AuthAPI.login(loginData).then((data)=>{const{ tokenType, accessToken }= data;
        localStorage.setItem(TOKEN_KEY, tokenType +" "+ accessToken);// Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxxresolve();}).catch((error)=>{reject(error);});});}

登录接口。

classAuthAPI{/** 登录 接口*/staticlogin(data: LoginData){const formData =newFormData();
    formData.append("username", data.username);
    formData.append("password", data.password);
    formData.append("captchaKey", data.captchaKey);
    formData.append("captchaCode", data.captchaCode);return request<any, LoginResult>({url:"/api/v1/auth/login",method:"post",data: formData,headers:{"Content-Type":"multipart/form-data",},});}// ...}

获取验证码。

/** 获取验证码 */functiongetCaptcha(){
  AuthAPI.getCaptcha().then((data)=>{
    loginData.value.captchaKey = data.captchaKey;
    captchaBase64.value = data.captchaBase64;});}

通过上述过程,我们已经成功获取 token 并存储在了 localStorage 中。

之后我们就可以根据 token “用户身份” 来进行权限控制了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

token 可以获取用户角色,而不同角色对应不同权限的路由,然后通过 router.addRoutes 动态挂载路由。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调用获取路由接口,获取路由表,进行动态路由处理,将其与常量路由进行拼接,得到总路由。

/**
 * 生成动态路由
 */functiongenerateRoutes(){returnnewPromise<RouteRecordRaw[]>((resolve, reject)=>{
    MenuAPI.getRoutes().then((data)=>{const dynamicRoutes =transformRoutes(data);
        routes.value = constantRoutes.concat(dynamicRoutes);resolve(dynamicRoutes);}).catch((error)=>{reject(error);});});}

这里使用 mock 数据:

exportdefaultdefineMock([{url:"menus/routes",method:["GET"],body:{code:"00000",data:[{path:"/doc",component:"Layout",redirect:"https://juejin.cn/post/7228990409909108793",name:"/doc",meta:{title:"平台文档",icon:"document",hidden:false,alwaysShow:false,params:null,},children:[{path:"internal-doc",component:"demo/internal-doc",name:"InternalDoc",meta:{title:"平台文档(内嵌)",icon:"document",hidden:false,alwaysShow:false,params:null,},},{path:"https://juejin.cn/post/7228990409909108793",name:"Https://juejin.cn/post/7228990409909108793",meta:{title:"平台文档(外链)",icon:"link",hidden:false,alwaysShow:false,params:null,},},],},{path:"/multi-level",component:"Layout",name:"/multiLevel",meta:{title:"多级菜单",icon:"cascader",hidden:false,alwaysShow:true,params:null,},children:[{path:"multi-level1",component:"demo/multi-level/level1",name:"MultiLevel1",meta:{title:"菜单一级",icon:"",hidden:false,alwaysShow:true,params:null,},children:[{path:"multi-level2",component:"demo/multi-level/children/level2",name:"MultiLevel2",meta:{title:"菜单二级",icon:"",hidden:false,alwaysShow:false,params:null,},children:[{path:"multi-level3-1",component:"demo/multi-level/children/children/level3-1",name:"MultiLevel31",meta:{title:"菜单三级-1",icon:"",hidden:false,keepAlive:true,alwaysShow:false,params:null,},},{path:"multi-level3-2",component:"demo/multi-level/children/children/level3-2",name:"MultiLevel32",meta:{title:"菜单三级-2",icon:"",hidden:false,keepAlive:true,alwaysShow:false,params:null,},},],},],},],},],msg:"一切ok",},},]);

转换路由数据为组件(根据实际业务进行弹性操作)。

/**
 * 转换路由数据为组件
 */consttransformRoutes=(routes: RouteVO[])=>{constasyncRoutes: RouteRecordRaw[]=[];
  routes.forEach((route)=>{const tmpRoute ={...route }as RouteRecordRaw;// 顶级目录,替换为 Layout 组件if(tmpRoute.component?.toString()=="Layout"){
      tmpRoute.component = Layout;}else{// 其他菜单,根据组件路径动态加载组件const component = modules[`../../views/${tmpRoute.component}.vue`];if(component){
        tmpRoute.component = component;}else{
        tmpRoute.component = modules[`../../views/error-page/404.vue`];}}if(tmpRoute.children){
      tmpRoute.children =transformRoutes(route.children);}

    asyncRoutes.push(tmpRoute);});return asyncRoutes;};

vue-element-admin 的实现方案

当然他们只是实现的写法不同,大致的思路还是相同的。

为了便于理解主要思路和提取关键代码,下面使用尚硅谷硅谷甄选项目的实现方案代码讲解(和vue-element-admin的大差不差)。

先看下用户信息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在用户 store 中过滤用户的异步路由(用路由的名字进行过滤区分-所以要保证名字的唯一):

//用于过滤当前用户需要展示的异步路由// asyncRoute 所有异步路由 routes 用户拥有权限的路由functionfilterAsyncRoute(asnycRoute: any,routes: any){return asnycRoute.filter((item: any)=>{if(routes.includes(item.name)){if(item.children && item.children.length >0){// 新的 item.children 也需要进行同样的过滤操作 
                item.children =filterAsyncRoute(item.children, routes);}returntrue;}})}

获取用户个人信息后再 store 中操作路由:

//计算当前用户需要展示的异步路由let userAsyncRoute =filterAsyncRoute(cloneDeep(asnycRoute), result.data.routes);//菜单需要的数路由数据this.menuRoutes =[...constantRoute,...userAsyncRoute, anyRoute];//目前路由器管理的只有常量路由,再异步获取路由路由信息后,异步路由、任意路由动态追加到路由管理中[...userAsyncRoute, anyRoute].forEach((route: any)=>{
    router.addRoute(route);});

路由表:

//对外暴露配置路由(常量路由):全部用户都可以访问到的路由exportconst constantRoute =[{//登录path:'/login',component:()=>import('@/views/login/index.vue'),name:'login',meta:{title:'登录',//菜单标题hidden:true,//代表路由标题在菜单中是否隐藏  true:隐藏 false:不隐藏icon:"Promotion",//菜单文字左侧的图标,支持element-plus全部图标}},{//登录成功以后展示数据的路由path:'/',component:()=>import('@/layout/index.vue'),name:'layout',meta:{title:'',hidden:false,icon:''},redirect:'/home',children:[{path:'/home',component:()=>import('@/views/home/index.vue'),meta:{title:'首页',hidden:false,icon:'HomeFilled'}}]},{//404path:'/404',component:()=>import('@/views/404/index.vue'),name:'404',meta:{title:'404',hidden:true,icon:'DocumentDelete'}},{path:'/screen',component:()=>import('@/views/screen/index.vue'),name:'Screen',meta:{hidden:false,title:'数据大屏',icon:'Platform'}}]//异步路由exportconst asnycRoute =[{path:'/acl',component:()=>import('@/layout/index.vue'),name:'Acl',meta:{title:'权限管理',icon:'Lock'},redirect:'/acl/user',children:[{path:'/acl/user',component:()=>import('@/views/acl/user/index.vue'),name:'User',meta:{title:'用户管理',icon:'User'}},{path:'/acl/role',component:()=>import('@/views/acl/role/index.vue'),name:'Role',meta:{title:'角色管理',icon:'UserFilled'}},{path:'/acl/permission',component:()=>import('@/views/acl/permission/index.vue'),name:'Permission',meta:{title:'菜单管理',icon:'Monitor'}}]},{path:'/product',component:()=>import('@/layout/index.vue'),name:'Product',meta:{title:'商品管理',icon:'Goods',},redirect:'/product/trademark',children:[{path:'/product/trademark',component:()=>import('@/views/product/trademark/index.vue'),name:"Trademark",meta:{title:'品牌管理',icon:'ShoppingCartFull',}},{path:'/product/attr',component:()=>import('@/views/product/attr/index.vue'),name:"Attr",meta:{title:'属性管理',icon:'ChromeFilled',}},{path:'/product/spu',component:()=>import('@/views/product/spu/index.vue'),name:"Spu",meta:{title:'SPU管理',icon:'Calendar',}},{path:'/product/sku',component:()=>import('@/views/product/sku/index.vue'),name:"Sku",meta:{title:'SKU管理',icon:'Orange',}},]}]//任意路由exportconst anyRoute ={//任意路由path:'/:pathMatch(.*)*',redirect:'/404',name:'Any',meta:{title:'任意路由',hidden:true,icon:'DataLine'}}

路由器对象(初始化的时候只注册了常量路由):

//创建路由器let router =createRouter({//路由模式hashhistory:createWebHashHistory(),routes: constantRoute,//滚动行为scrollBehavior(){return{left:0,top:0}}});

路由鉴权守卫(这里在某些业务情况下可以增加白名单,对权限进行再一次划分):

//路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)import router from'@/router';import setting from'./setting';import nprogress from'nprogress';//引入进度条样式import"nprogress/nprogress.css";
nprogress.configure({showSpinner:false});//获取用户相关的小仓库内部token数据,去判断用户是否登录成功import useUserStore from'./store/modules/user';import pinia from'./store';let userStore =useUserStore(pinia);//全局守卫:项目当中任意路由切换都会触发的钩子//全局前置守卫
router.beforeEach(async(to: any,from: any,next: any)=>{
    document.title =`${setting.title} - ${to.meta.title}`//to:你将要访问那个路由//from:你从来个路由而来//next:路由的放行函数
    nprogress.start();//获取token,去判断用户登录、还是未登录let token = userStore.token;//获取用户名字let username = userStore.username;//用户登录判断if(token){//登录成功,访问login,不能访问,指向首页if(to.path =='/login'){next({path:'/'})}else{//登录成功访问其余六个路由(登录排除)//有用户信息if(username){//放行next();}else{//如果没有用户信息,在守卫这里发请求获取到了用户信息再放行try{//获取用户信息await userStore.userInfo();//放行//万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果next({...to,replace:true})}catch(error){//token过期:获取不到用户信息了//用户手动修改本地存储token//退出登录->用户相关的数据清空await userStore.userLogout();next({path:'/login',query:{redirect: to.path }})}}}}else{//用户未登录判断if(to.path =='/login'){next();}else{next({path:'/login',query:{redirect: to.path }});}}})//全局后置守卫
router.afterEach((to: any,from: any)=>{
    nprogress.done();});//第一个问题:任意路由切换实现进度条业务 ---nprogress//第二个问题:路由鉴权(路由组件访问权限的设置)//全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)//用户未登录:可以访问login,其余六个路由不能访问(指向login)//用户登录成功:不可以访问login[指向首页],其余的路由可以访问

这里用到了 next({…to,replace:true}) ,它的原理是:

router.addRoutes是同步方法,整体流程:

  1. 路由跳转,根据目标地址从router中提取route信息,由于此时还没addRouters,所以解析出来的route是个空的,不包含组件。
  2. 执行beforeEach钩子函数,然后内部会动态添加路由,但此时route已经生成了,不是说router.addRoutes后,这个route会自动更新,如果直接next(),最终渲染的就是空的。
  3. 调用next({ …to, replace: true }),会abort刚刚的跳转,然后重新走一遍上述逻辑,这时从router中提取的route信息就包含组件了,之后就和正常逻辑一样了。 主要原因就是生成route是在执行beforeEach钩子之前。

上述解释摘自手摸手,带你用vue撸后台 系列二(登录权限篇) - 掘金 (juejin.cn)评论区

页面权限总结

页面权限:

  1. 用户登录后,服务端返回一个权限树(用树形结构呈现权限数据),然后我们去解析这个树形结构,得到我们需要的路由表(动态路由对象),本质上就是一个由路由对象为元素的数组。
  2. 然后通过 vue 中的动态路由,也就是 addRoutes,动态的添加路由。
  3. 最后根据路由去渲染多级菜单栏。

按钮权限

可以自定义一个全局指定,用于按钮权限的判断。

import pinia from'@/store';import useUserStore from'@/store/modules/user';let userStore =useUserStore(pinia)exportconstisHasButton=(app: any)=>{//获取对应的用户仓库//全局自定义指令:实现按钮的权限
    app.directive('has',{//代表使用这个全局自定义指令的DOM|组件挂载完毕的时候会执行一次mounted(el:any,options:any){//自定义指令右侧的数值:如果在用户信息buttons数组当中没有//从DOM树上干掉if(!userStore.buttons.includes(options.value)){
               el.parentNode.removeChild(el);}},})}

el 为该元素,options 为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 app.ts 中引入。

import{isHasButton}from'@/directive/has.ts';isHasButton(app);

使用:

v-has="`btn.Trademark.add`"

按钮(功能)权限总结

服务端返回的权限树中包含了指定页面下指定按钮的数据,可以通过 v-if 或者 disable 来控制按钮权限。

接口权限

配合功能权限,一般由服务端进行处理。


本文转载自: https://blog.csdn.net/XiugongHao/article/details/140767530
版权归原作者 小秀_heo 所有, 如有侵权,请联系我们删除。

“【Vue】权限控制”的评论:

还没有评论