Vue权限控制
1.权限相关概念
1.1 权限的分类
-
后端权限
从根本上来讲,前端仅仅是视图层的战事,权限的核心在于服务器中数据的变化,所以后端才是权限的关键。
后端权限可以控制某个用户是否能够查询数据,是否能够修改数据库的操作。
-
后端如何知道请求是哪个用户发过来的
cookie session token -
后端的权限设计RBAC
用户 角色 权限
-
-
前端权限
前端权限的控制,本质上来说就是控制前端的视图层的展示和前端所发送的请求,但是只有去前端权限控制没有后端权限控制,是万万不可以的。前端权限控制只是锦上添花的效果。
1.2 前端权限的意义
如果金葱能够修改服务器数据库中的数据层面来讲,确实只在后端控制就足够了,那为什么越来越多的翔宇也进行了前端权限的控制,主要有几个方面:
2.前端权限控制思路
2.1菜单的控制
在登录和请求中,会得到权限数据,当然这个需要后端数据的支持。前端根据权限数据,展示对应的菜单,点击菜单,才能查看相关的界面。
2.2 界面的控制
如果用户没有登录,手动在地址栏中敲入管理界面的地址,则需要跳转到登录界面
如果用户已经登录,可是手动敲入非权限内的地址,则需要跳转到404界面
2.3 按钮的控制
在某个菜单的界面中,还得根据权限数据,展示可以进行操作的按钮,比如删除、修改和增加
2.4 请求和响应的控制
如果用户通过非常规操作,比如通过浏览器调试工具,将某些禁用按钮变成启用状态,此时发送的请求,也应该被前端所拦截。
3.Vue的权限控制实现
3.1菜单控制
-
登录之后获取到的数据
{ "meta": { "status": 200 }, "data": { "id": "350000199209068278", "rid": 50, "username": "admin", "mobile": "@mobile()", "email": "k.tubllkvk@oii.ph", "token": "#[[%#&@&[)*#^[[$&(&^]!%^#[]![@[)(([#%%%$]$((&%^[[", "avatar": "http://dummyimage.com/20×20/red/fff&text=avatar" }, "rights": [ { "id": 125, "authName": "用户管理", "icon": "el-icon-user-solid", "children": [ { "id": 110, "authName": "用户列表", "path": "users", "rights": [ "view", "edit", "add", "delete" ] } ] }, { "id": 103, "authName": "角色管理", "icon": "el-icon-setting", "children": [ { "id": 111, "authName": "角色列表", "path": "roles", "rights": [ "view", "edit", "add", "delete" ] } ] }, { "id": 101, "authName": "商品管理", "icon": "el-icon-chat-line-square", "children": [ { "id": 1001, "authName": "商品列表", "path": "goods", "rights": [ "view", "edit", "add", "delete" ] }, { "id": 1002, "authName": "商品分类", "path": "categories", "rights": [ "view", "edit", "add", "delete" ] } ] } ] } -
刷新界面菜单消失
-
原因分析
因为菜单数据是登录之后才获取到的,获取菜单数据之后,就存放在vuex中 一旦刷新界面,Vuex中的数据会重新初始化,所以会变成空的数组 因此,需要将权限数据存储在sessionStorage中,并让其和vuex中的数据保持同步 -
代码解决
/store/index.js
import Vue from "vue"; import Vuex from "vuex"; import persist from "vuex-persistedstate"; Vue.use(Vuex); export default new Vuex.Store({ state: { rightList: JSON.parse(sessionStorage.getItem("rightList") || "[]"), username: "", }, getters: {}, mutations: { setRightList(state, data) { state.rightList = data; }, setUsername(state, data) { state.username = data; }, setToken(state, data) { state.token = data; }, }, actions: {}, modules: {}, plugins: [persist()], });
-
3.2界面控制
1.正常的逻辑是通过登录界面,登录成功之后跳转到管理平台界面,但是如果用户直接敲入管理平台的地址,也是可以跳过登录的步骤,所以应该在某个时机判断用户是否登录。
-
如何判断是否登录
-
什么时机
-
路由导航守卫
/router/index.js
router.beforeEach((to, from, next) => { if (to.path == "/login") { next(); } else { let { token } = JSON.parse(localStorage.getItem("vuex") || "[]"); if (!token) { next("/login"); } else { next(); } } });
-
2.虽然菜单项目已经被控制住了,但是路由信息还是完成存在于浏览器,比如zhangsan这个用户并不具备管理商品和商品分类的权限,但是如果他自己在地址栏中敲入/goods 或者 /roles的地址,依然可以访问角色界面。
-
路由导航守卫
-
动态路由
-
登录成功之后动态添加
-
App.vue中添加
<template> <div id="app"> <router-view /> </div> </template> <script> import { initDynamicRoutes } from "./router"; export default { name: "app", created() { initDynamicRoutes(); }, }; </script> -
/router.js
import Vue from "vue"; import VueRouter from "vue-router"; import Login from "../views/Login.vue"; import Home from "../views/Home.vue"; import Welcome from "../views/Welcome.vue"; import Users from "../views/Users.vue"; import Roles from "../views/Roles.vue"; import GoodsList from "../views/GoodsList.vue"; import GoodsCate from "../views/GoodsCate.vue"; import NotFound from "../views/NotFound.vue"; import store from "@/store"; Vue.use(VueRouter); //动态添加路由 const userRule = { path: "/users", component: Users }; const roleRule = { path: "/roles", component: Roles }; const goodRule = { path: "/goods", component: GoodsList }; const categoryRule = { path: "/categories", component: GoodsCate }; const ruleMapping = { users: userRule, roles: roleRule, goods: goodRule, categories: categoryRule, }; const routes = [ { path: "/", redirect: "/home", }, { path: "/login", component: Login, }, { path: "/home", component: Home, redirect: "/welcome", children: [ { path: "/welcome", component: Welcome }, // { path: "/users", component: Users }, // { path: "/roles", component: Roles }, // { path: "/goods", component: GoodsList }, // { path: "/categories", component: GoodsCate }, ], }, { path: "*", component: NotFound, }, ]; const router = new VueRouter({ mode: "history", base: process.env.BASE_URL, routes, }); router.beforeEach((to, from, next) => { if (to.path == "/login") { next(); } else { let { token } = JSON.parse(localStorage.getItem("vuex") || "[]"); if (!token) { next("/login"); } else { next(); } } }); //动态修改路由 export function initDynamicRoutes() { const currentRoutes = router.options.routes; const rightList = store.state.rightList; rightList.forEach((item) => { item.children.forEach((item) => { const temp = ruleMapping[item.path]; temp.meta = item.rights; currentRoutes[2].children.push(temp); }); }); router.addRoutes(currentRoutes); } export default router;
-
3.3按钮控制
-
效果图
-
通过自定义指令,给button添加自定义指令v-permission,控制按钮的可用和不可用。
admin拥有管理用户的所有权限,包括对用户的增删改查 zhangsan只拥有查看用户的权限,不能增、删、改 -
在入口文件中引入自定义指令
/main.js
import "./utils/permission.js"; -
添加自定义指令v-permission。如果
/utils/permission.js
import Vue from "vue"; import router from "@/router"; Vue.directive("permission", { inserted: function (el, binding) { const action = binding.value.action; const currentRight = router.currentRoute.meta; if (currentRight) { if (currentRight.indexOf(action) == -1) { const type = binding.value.effect; if (type === "disabled") { el.disabled = true; el.classList.add("is-disabled"); } else { el.parentElement.removeChild(el); } } } }, }); -
在用户列表中使用自定义组件
/views/Users.vue
<el-table-column label="操作"> <template slot-scope="scope"> <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)" v-permission="{ action: 'view' }" >查看</el-button > <el-button type="primary" size="mini" @click="handleEdit(scope.$index, scope.row)" v-permission="{ action: 'edit', effect: 'disabled' }" >编辑</el-button > <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)" v-permission="{ action: 'delete', effect: 'delete' }" >删除</el-button > </template>
3.4请求控制
-
除了登录请求都得带上token,这样服务器才能鉴别你的身份
/utils/axios.js
axios.interceptors.request.use(function (req) { const currentUrl = req.url; if (currentUrl !== "login") { let { token } = JSON.parse(localStorage.getItem("vuex")); req.headers.Authorization = token; } -
如果发出了非权限内的请求,应该直接在前端访问内组织,虽然这个请求发送到服务器也会被拒绝
import Vue from "vue"; import axios from "axios"; import router from "@/router"; const actionMapping = { get: "view", post: "add", put: "edit", delete: "delete", }; axios.interceptors.request.use(function (req) { const currentUrl = req.url; if (currentUrl !== "login") { let { token } = JSON.parse(localStorage.getItem("vuex")); req.headers.Authorization = token; } /** * get请求 查看 * post请求 增加 * put请求 修改 * delete请求 删除 */ const method = req.method; const action = actionMapping[method]; const rights = router.currentRoute.meta; if (rights && rights.indexOf(action) == -1) { this.$message.warn("没有权限"); } return req; }); axios.interceptors.response.use(function (res) { return res; }); Vue.prototype.$axios = axios;
响应控制
-
得到了服务器返回状态码401,代表token超时或者被篡改了,这时候应该强制跳转到登录界面
axios.interceptors.response.use(function (res) { if (res.data.meta.status === 401) { router.push("/login"); sessionStorage.clear(); window.location.reload(); } return res; });
4.小结
前端权限的控制必须要后端提供数据支持,否则无法实现
返回的权限数据的结构,前后端需要沟通协商,怎样的数据使用起来才最方便
4.1菜单控制
- 权限的数据需要多组件之间共享,因此采用vuex
- 防止刷新界面,权限数据丢失,所以需要存储在sessionStorage,并且要保证两者的同步
4.2界面控制
- 路由的导航守卫可以防止跳过登录界面
- 动态路由可以让不具备权限的界面的路由规则压根就不存在
4.3按钮控制
- 路由规则中可以增加路由元数据meta
- 通过路由对象可以得到当前的路由规则,以及存储在此规则中的meta数据
- 自定义指令可以很方便的实现按钮控制
4.4请求和响应控制
- 请求拦截器和响应拦截器的使用
- 请求方式的约定restfull