vue之权限控制和动态路由

3,276 阅读2分钟

思路


  • 登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后(我会将这个token存贮到localStore中,保证刷新页面后能记住用户登录状态),前端会根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
  • 权限验证:通过token获取用户对应的 role,动态根据用户的 role 算出其对应有权限的路由,通过 router.addRoutes 动态挂载这些路由。

路由定义


路由分为两种:constantRoutesasyncRoutes

constantRoutes : 代表那些不需要动态判断权限的路由,如登录页、通用页等。

asyncRoutes : 代表那些需要动态判断权限并通过addRoutes动态添加的页面。

创建router.js


import Vue from "vue";
import VueRouter from "vue-router";
import Layout from "@/layout";

Vue.use(VueRouter);

//通用页面:不需要守卫,可直接访问
export const constRoutes = [
  {
    path: "/login",
    component: () => import("@/views/Login.vue"),
    hidden: true //导航菜单忽略该项
  },
  {
    path: "/",
    component: Layout, //应用布局
    redirect: "/home",
    alwaysShow: true,
    meta: {
      title: "客户管理",  //导航菜单项标题
      icon:"kehu" //导航菜单项图标
    },
    children: [
      {
        path: "/home",
        component: () => import("@/views/Home.vue"),
        name: "home",
        meta: {
          title: "客户列表"
        }
      }
    ]
  }
];

//权限页面:受保护页面,要求用户登录并拥有访问权限的角色才能访问
export const asyncRoutes = [
  {
    path: "/system_manage",
    component: Layout,
    redirect: "/system_set",
    meta: {
      title: "系统设置",
      icon: "set"
    },
    children: [	
      {
        path: "/system_set",
        component: () => import("@/views/system_set.vue"),
        name: "system_set",
        meta: {
          title: "系统设置",
          roles: ["admin", "editor"] // 设置该路由进入的权限,支持多个权限叠加
        }
      },
      {
        path: "/system_organiza",
        component: () => import("@/views/system_origaniza.vue"),
        name: "system_origaniza",
        meta: {
          title: "组织结构",
          roles: ["admin"]
        },
        children:[//三级路由嵌套,还要手动在二级目录的根文件下添加一个 <router-view />
          {path:'/custom_link',name:'custom_link',component:() => import("@/views/custom_link.vue"),meta:{title:'客户联系人'}},
          {path:'/tracking',name:'tracking',component:() => import("@/views/tracking.vue"),meta:{title:'跟踪记录'}}
        ]
      },
      {
        path: "/system_data",
        component: () => import("@/views/system_data.vue"),
        name: "system_data",
        meta: {
          title: "数据字典",
          roles: ["admin"]
        }
      }
    ]
  }
];

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

export default router;


登录


创建登录页 views/Login.vue

<template>
  <div class="container">
    <h2>用户登录</h2>
    <input type="text" v-model="username" />
    <button @click="login">登录</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: ""
    };
  },
  methods: {
    login() {
      /*this.$store
        .dispatch("user/login", { username: this.username })
        .then(() => {
          this.$router.push({
            //   接受路由参数然后跳转
            path: this.$route.query.redirect || "/"
          });
        })
        .catch(error => {
          alert(error);
        });*/
        //调api获取token
    }
  }
};
</script>

用户登陆状态维护


vuex根模块实现,./store/index.js

import Vue from "vue";
import Vuex from "vuex";
import user from "./modules/user";
import permission from "./modules/permission";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: { user, permission },
  getters: {
    roles: state => {
      return state.user.roles;
    }
  }
});

user模块-存储token 和 roles ./store/modules/user.js

const state = {
  token: localStorage.getItem("token"),
  roles: []
};

const mutations = {
  SET_TOKEN: (state, token) => {
    state.token = token;
  },
  SET_ROLES: (state, roles) => {
    state.roles = roles;
  }
};

const actions = {
  login({ commit }, userinfo) {
    const { username } = userinfo;
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (username === "admin" || username === "jerry") {
          commit("SET_TOKEN", username);
          localStorage.setItem("token", username);
          resolve();
        } else {
          reject("用户名、密码错误");
        }
      }, 1000);
    });
  },
  getInfo({ commit, state }) {
    return new Promise(resolve => {
      setTimeout(() => {
        const roles = state.token === "admin" ? ["admin"] : ["editor"];
        commit("SET_ROLES", roles);
        resolve(roles);
      }, 1000);
    });
  }
};

export default {
  namespaced: true,
  state,
  mutations,
  actions
};

路由守卫


创建./src/permission.js

import router from "./router";
import store from "./store";

const whiteList = ["/login"]; //无需令牌白名单

router.beforeEach(async (to, from, next) => {
  //to and from are Route Object,next() must be called to resolve the hook
  // 获取令牌判断用户是否登录
  const hasToken = localStorage.getItem("token");
  if (hasToken) {
    //已登录
    if (to.path === "/login") {
      //若以登录没有必要显示登录页,重定向回首页
      next({ path: "/" });
    } else {
      // 去其他路由
      const hasRoles =
        store.state.user.roles && store.state.user.roles.length > 0;
      if (hasRoles) {
        // 若用户角色已付加则说明权限以判定,动态路由已添加
        next();
      } else {
        try {
          // 请求获取用户信息
          const roles = await store.dispatch("user/getInfo");
          console.log(roles);
          // 根据当前用户角色动态生成路由
          const accessRoutes = await store.dispatch(
            "permission/generateRoutes",
            roles
          );
          console.log(accessRoutes);
          // 添加这些至路由器
          router.addRoutes(accessRoutes);
          // 继续路由切换,确保addRoutes完成
          next({ ...to });
        } catch (error) {
          // 出错需要重置令牌(令牌过期,网络错误等原因)
          //await store.dispatch('user/resetToken')
          next(`/login?redirect=${to.path}`);
          alert(error || "未知错误");
        }
      }
    }
  } else {
    //未登录
    if (whiteList.indexOf(to.path) !== -1) {
      // 白名单中的路由路过
      next();
    } else {
      // 重定向至登录页
      next(`/login?redirect=${to.path}`);
    }
  }
});

添加动态路由


根据用户角色过滤出可访问路由并动态添加到router 创建permission模块,store/modules/permission.js

import { constRoutes, asyncRoutes } from "@/router";

const state = {
  routes: [], //完整路由表
  addRoutes: [] //用户可访问路由表
};

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes;
    state.routes = constRoutes.concat(routes);
  }
};

const actions = {
  // 路由生成:在得到用户角色后第一时间调用
  generateRoutes({ commit }, roles) {
    return new Promise(resolve => {
      // 根据角色做过滤处理
      const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
      commit("SET_ROUTES", accessedRoutes);
      resolve(accessedRoutes);
    });
  }
};

/**
 * 递归过滤AsyncRoutes路由表
 * @routes 带过滤的路由表,首次传入的就是AsyncRoutes
 * @roles  用户拥有角色
 */
export function filterAsyncRoutes(routes, roles) {
  const res = [];
  routes.forEach(route => {
    // 复制一份
    const tmp = { ...route };
    // 如果用户有访问权限则加入结果路由表
    if (hasPermission(roles, tmp)) {
      // 如果存在子路由则递归过滤之
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles);
      }
      res.push(tmp);
    }
  });
  return res;
}

/**
 * 根据路由meta.role确定是否当前用户拥有访问权限
 * @roles 用户拥有的角色
 * @route 待判定路由
 */

export function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    //  若用户拥有的角色中有被包含在待判定的路由角色表中则拥有访问权
    return roles.some(role => route.meta.roles.includes(role));
  } else {
    //  没有设置roles则无需判定即可访问
    return true;
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
};

异步获取路由表


用户登录后向后端请求可访问的路由表,从而动态生成可访问页面,操作和原来是相同的,这里多了一步将后端返回路由表中组件名称和本地的组件映射步骤:

//前端的映射表map就是之前的asyncRoutes
//服务端返回的map类似于
const serviceMap = [
	{path:'/login',component:'login',hidden:true}
]
//遍历serviceMap,将component替换为map[component],动态生成asyncRoutes
function mapComponent(serviceMap){
	serviceMap.forEach(route => {
    route.component = map[route.component];
    if(route.children){
    	route.children.map(child => mapComponent(child))
      }
	})
}
mapComponent(serviceMap)

按钮权限

封装一个指令v-permission,从而实现按钮级别权限控制,创建src/directtive/permission.js

自定义指令参考 cn.vuejs.org/v2/guide/cu…

import store from "@/store";
const permission = {
  inserted(el, binding) {
    // 获取指令的值:按钮要求的角色数组
    const { value: pRoles } = binding;
    // 获取用户角色
    const roles = store.getters && store.getters.roles;
    if (pRoles && pRoles instanceof Array && pRoles.length > 0) {
      const hasPermission = roles.some(role => {
        return pRoles.includes(role);
      });
      // 如果没有权限删除当前dom
      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el);
      }
    } else {
      throw new Error(
        `需要指定按钮要求角色数组,如v-permission="['admin','editor']"`
      );
    }
  }
};
export default permission;

注册指令 main.js

import vPermission from "./directive/permission";
Vue.directive("permission", vPermission);

测试

<button v-permission="['admin', 'editor']">admin editor</button>
<button v-permission="['admin']">admin</button>

该指令只能删除挂在指令的元素,对于那些额外生成的和指令无关的元素无能为力,比如:挂载在tab上只能删除标签,无法删除对应面板。 可以使用全局权限判断函数,使用v-if实现

<template>
	<el-tab-pane v-if="checkPermission(['admin'])"></el-tab-pane>
</template>
<script>
export default{
	methods:{
    	checkPermission(permissionRoles){
        	return roles.some(role => {
            	return permissionRoles.include(role);
            });
        }
    }
}
</script>