vue 动态路由/路由权限 解决方案

路由权限思路:

1.菜单栏/导航栏(一级权限)
在登录成功后,获取后端的权限数据, 根据权限数据,展示对应的路由导航或菜单即可;


登录成功后, 获取的该账号 的路由权限数组

2.界面的控制
如果用户没有登录, 用户手动在地址栏输入路由地址,则需要跳转到登录界面.
如果用户已经登录, 用户手动输入的非权限内的路由地址,则给他跳转到404界面.

3.按钮的控制(二级权限)
在页面中,有些账号有: 添加,删除,修改,增加等权限, 有些没有.
没有权限的账号在这个页面只是可以浏览页面中的数据,需要对这些按钮 禁用or隐藏。

4.请求和响应的控制 (这一步其实后端也会根据token判断身份信息, 来返回数据,可以省略)
如果用户通过非常规的手段(可能是同行), 比如通过浏览器f12将禁用的按钮disabled 改成false, 隐藏的按钮apacity:0改成了1,这些按钮就可以使用了,此时需要对按钮点击后发出的请求作出拦截.


login.vue

    login(){
        var accountInfo= {
            "data":{
                "meta":{"msg":"登录成功","status":200},
                "username":"admin",
                "token":" ***** ",
            },
        };
        
        var rights = {
            "meta":{"msg":"获取菜单列表成功","status":200},
            "data":
            [ {
                    "id":125,
                    "authName":"用户管理",
                    "path":"users",
                    "children":[
                        {
                            "id":110,
                            "authName":"用户列表",
                            "path":"users",
                            "children":[],
                            "rights": ['add', 'delete'],  //二级权限  可添加, 可删除
                        }
                    ],
                    "order":1
                }, {
                    "id":103,
                    "authName":"权限管理",
                    "path":"rights",
                    "children":[
                        {
                            "id":111,
                            "authName":"角色列表",
                            "path":"roles",
                            "children":[],
                            "rights": ['add'],      //二级权限  可添加
                        },{
                            "id":112,
                            "authName":"权限列表",
                            "path":"rights",
                            "children":[],
                            "rights": ['edit'],    //二级权限  可编辑
                        }
                    ],
                    "order":2
                },
            ],
        }
        sessionStorage.setItem("token",accountInfo.data.token);         //存储获取到的token
        this.$store.commit('setUsername', accountInfo.data.username);
        this.$store.commit('setRightList', rights.data);                //存储路由权限 数据数组

        initDynamicRoutes();                                            //登录成功后执行动态路由添加
        this.$router.push('/home');                                     //跳入到主页
    }

home.vue (elment-ui)

 <template>
  <div class="home-wrapper">
    <el-aside :width="isCollapse? '64px':'200px'">

       <el-menu :default-active="activePath" :router="true" :collapse-transition="false" :collapse="isCollapse" :unique-opened="true" background-color="#ccc" text-color="#fff" active-text-color="#409EFF">            
                                             <!-- 根据登录后获取到的权限 循环出来即可 -->          
          <el-submenu :index="item.id + '' " v-for="item in menulist" :key="item.id">
                  <template slot="title">
                          <i :class="iconsObj[item.id]"></i>     <!-- 显示图标 --> 
                          <span>{{item.authName}}</span>         <!-- 显示文本 -->
                  </template>
        
                  <el-menu-item  :index="'/' + subItem.path"  v-for="subItem in item.children"  :key="subItem.id"  @click=" handleNavState('/' + subItem.path) ">
                           <i class="el-icon-location"></i>
                           <span>{{subItem.authName}}</span>
                  </el-menu-item>
          </el-submenu>
       </el-menu>

    </el-aside>
     <el-main>
          <router-view></router-view>           
     </el-main>
  </div>
</template>

<script>
import {mapState} from 'vuex';
    export default {
      data(){
        return{
            menulist:[],
            iconsObj:{             //匹配权限id  显示对应图标
                '125':"iconfont icon-user",
                '103':"iconfont icon-tijikongjian",
                '101':"iconfont icon-shangpin",
                '102':"iconfont icon-danju",
                '145':"iconfont icon-baobiao"
            },
            isCollapse:false,     //是否可伸缩
            activePath:''         //当前选中
        }
      },
      created(){
          //刷新or重新进入页面,高亮显示之前点击的路由,没有就默认高亮显示/users这个页面
          this.activePath = sessionStorage.getItem('activePath') || '/users';  
          this.menulist = this.rightList;
      },
      computed:{
          ...mapState(['rightList', 'username'])                //获取vuex中路由权限数组 和 用户名
      },
      methods: {
        //退出登录
        logOut() {   
              sessionStorage.clear();
              this.$router.push("/login");
              window.location.reload();        //重新刷新  解除vuex的数据
        },
        //侧边栏伸缩
        toggleCollapse(){                                       
             this.isCollapse = !this.isCollapse;
        },
        //当前选中的item栏,高亮显示,  存储点击过的路由,防止刷新重置
        handleNavState(activePath){                           
              this.activePath = activePath;
              sessionStorage.setItem('activePath',activePath);  
        },
    }
}
</script>

store.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
      state: {
                            //获取sessionStorage中的数据, 防止vuex刷新之后为空数组
            rightList:   JSON.parse( sessionStorage.getItem('rightList')) || [],
            username:    sessionStorage.getItem('username') || '',
      },
      mutations: {
            setRightList(state, data){
                state.rightList = data;
                sessionStorage.setItem("rightList",JSON.stringify(data));  
            },
            setUsername(state, data){
                state.username = data;
                sessionStorage.setItem("username",data);
            },
    },
    actions: { },
    modules: { },
})

router.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store/index.js'
Vue.use(VueRouter)

const Login =     () => import('@/components/login.vue');
const Home  =     () => import('@/components/home.vue');
const welcome =   () => import('@/components/welcome.vue');

const User   =     () => import('@/components/user.vue');
const Rights =     () => import('@/components/rights.vue');
const Roles  =     () => import('@/components/roles.vue');

const Cate   =     () => import('@/components/cate.vue');
const Params =     () => import('@/components/params.vue');

const List   =     () => import('@/components/list.vue');
const Add    =     () => import('@/components/add.vue');

const Order   =     () => import('@/components/order.vue');
const Report  =     () => import('@/components/report.vue');
const notFound   = () => import('@/components/notFound.vue');

const userRule =     { path:'/users',  component:User };
const roleRule =     { path:'/roles', component:Roles };
const goodRule =     { path:'/goods',  component:List  };
const categoryRule = { path:'/categories', component:Cate };
const rightRule =    { path:'/rights', component:Rights };
const paramRule =    { path:'/params',  component:Params };
const addRule =      { path:'/goods/add',  component:Add };
const orderRule =    { path:'/orders', component:Order };
const reportRule =   { path:'/reports', component:Report };

const ruleMapping = {             //匹配路由名字获得对应组件
        'users':    userRule,
        'roles':    roleRule,
        'goods':    goodRule,
        'rights':   rightRule,
        'params':   paramRule,
        'orders':   orderRule,
        'reports':  reportRule,
        'categories':categoryRule,
}


const routes = [
  {
      path:'/',
      redirect:'/login'
 },
 {
      path: '/login',
      name: 'login',
      component: Login
  },
  {
      path: '/home',
      name: 'home',
      component: Home,
      redirect:"/welcome",
      children:[]
  },
  {
      path:'*',            //思路2.界面的控制:   用户手动输入任何不匹配的路由地址,就转到notFound页
      component:notFound
  }
]

const router = new VueRouter({
      mode: 'history',
      base: process.env.BASE_URL,
      routes
  });

//动态添加路由
export function initDynamicRoutes(){
   console.log(router);

   const currentRoutes = router.options.routes;
   const rightList = store.state.rightList;            //获取vuex中路由权限数组

   rightList.forEach( item => {
        item.children.forEach(item =>{
             const temp = ruleMapping[item.path];      //↑↑匹配路由名字获取对应组件
             temp.meta = item.rights                   //将二级权限添加到 meta对象中.  思路4:请求&响应的控制就是通过匹配meta中的权限来操作的

             currentRoutes[2].children.push(temp);     //添加到 /home的childern中作为子路由
        })
   })
   router.addRoutes(currentRoutes);
}

//路由导航守卫
//除了登录之外的其他有权限的接口,通过token来身份验证访问;
router.beforeEach((to,from,next)=>{
    if(to.path === '/login'){
        next();
    }else{
        const token = sessionStorage.getItem('token');  //通过token值判断用户是否是登录状态
        if(!token){
            next('/login')
        }else{
            next();
        }
    }
})
export default router

动态路需两个地方调用:
1 login.vue 中登录成功后立即执行动态路由函数
2 app.vue中, 在根组件中添加执行动态路由函数,这样每一次用户刷新,就会执行,否则刷新之后,动态路由就没了

<template>
  <div id="app">
         <router-view></router-view>
  </div>
</template>
<script>
import { initDynamicRoutes } from '@/router/index.js'
export default {
  name: 'app',
  created(){
     initDynamicRoutes();
   }
}
</script>

思路3:按钮的控制(二级权限): 这里我是通过 自定义指令来实现 对按钮(增,删,改等二级权限按钮) 的显示隐藏。
更简单直接的方式,直接就在html中对button添加v-if显示隐藏即可
import './utils/permission.js'

import Vue from 'vue';
import router from '@/router/index.js'

Vue.directive('permission',{
    inserted(el, binding){      //el当前元素,  binding是一个对象,包含了某些属性
        const action = binding.value.action;        //匹配按钮类型     'add'? 'delete'? 'edit'?
        const effect = binding.value.effect;        //达到什么样的效果

        console.log(router.currentRoute.meta);      //获取到当前路由下源数据meta数组

        if(router.currentRoute.meta.indexOf(action) == -1){
            el.parentElement.removeChild(el);       //如果没有该权限,当前元素的父节点就移除该按钮节点元素

        }else{

            if(effect = 'isDisabled'){      //想要的效果是 不可选状态
                el.disabled = true;
                el.classList.add('is-disabled')    // ←这里是element-ui特定的添加disabled方式:就是给button添加了disable属性 
            };
            if(effect=='removeNode'){       //想要的效果是删除该按钮
                el.parentElement.removeChild(el)
            } 
        }
    }
})

<el-button type="primary" @click="dialog" v-permission="{action:'add', effect: 'removeNode'}">添加用户</el-button>
<el-button type="primary" @click="handleDelete" v-permission="{action:'delete', effect: 'isDisabled' }">删除</el-button>

思路4:服务器返回状态码401, 代表token超时or被串改or未传token, 此时强制跳转到登录页重新登录

import router from '../router';
import Vue from "vue";


axios.interceptors.request.use(config => {  //在请求拦截器中,给每个请求头添加token
    NProgress.start();        //当请求时, 显示请求进度条(NProgress插件)

    config.headers.Authorization = sessionStorage.getItem('token');
    return config         
})

axios.interceptors.response.use(config=>{  //响应拦截器
    NProgress.done();
    const status = config.data.meta.status;
    if(status=== 401 || status === 403 || status === 404){
          router.push('/login');
          sessionStorage.clear();
          Vue.prototype.$message(config.data.message);
          location.reload();      //重新刷新释放vuex所有数据
    }
    return config;
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342