Vue权限控制

175 阅读3分钟

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界面控制

1670819796598

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按钮控制

  • 效果图

    1670819692594

  • 通过自定义指令,给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