前后端用户、角色、权限控制封装


实现思路

实现权限控制,主要是前后端两个部分,后端将具体的权限封装到数据库中,前端将请求所得的权限信息封装到动态路由中,用以控制菜单权限,但是在功能权限控制时,前端需要通过自定义指令控制。通过这一些列的操作,就能完成权限控制。实现用户权限控制主要需要解决以下问题:

  1. 数据库如何设计,数据库分为用户表、权限表、角色表、用户角色关联表、角色权限关联表,这也是用户权限控制固定的建表模式。
  2. 前端实现动态路由达到菜单权限控制。用户登录后,从后端获取当前用户所拥有的菜单权限列表,将其加入到动态路由中,最好的就是后端返回拿来即用,无需二次封装。
  3. 前端实现功能控制,前端封装自定义指令,通过判断当前功能是否在功能权限列表中,是的展示,不是的隐藏。当然功能权限列表也是通过后端封装。

后端表设计

精简化的数据库表设计。

用户表

CREATE TABLE `t_user` (
  `id` bigint NOT NULL,
  `username` varchar(255) NOT NULL COMMENT '用户名',
  `password` varchar(255) NOT NULL COMMENT '用户密码',
  `nick_name` varchar(255) NOT NULL COMMENT '昵称',
  `phone` varchar(255) NOT NULL COMMENT '手机号码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

角色表

CREATE TABLE `t_role` (
  `id` bigint NOT NULL,
  `role_name` varchar(255) NOT NULL COMMENT '角色名',
  `role_status` tinyint NOT NULL COMMENT '角色状态(1:启用,2:关闭)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表';

权限表

CREATE TABLE `t_permission` (
  `id` bigint NOT NULL,
  `fid` int NOT NULL COMMENT '父节点ID',
  `name` varchar(255) NOT NULL COMMENT '权限名称',
  `type` tinyint NOT NULL COMMENT '权限类型(1:菜单权限,2:功能权限)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='权限表';

用户角色表

CREATE TABLE `t_user_role` (
  `uid` bigint NOT NULL COMMENT '用户ID',
  `rid` bigint NOT NULL COMMENT '角色ID'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色表';

角色权限表

CREATE TABLE `t_role_permission` (
  `rid` bigint NOT NULL COMMENT '角色ID',
  `pid` bigint NOT NULL COMMENT '权限ID'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色权限表';

用户登录后,获取的数据结构如下:

{
  "username": "admin",
  "nickname": "san·zhang",
  "id": "3957304583045",
  "permission": {
    // 菜单权限
    "menus": ["userManage", "articleManage", "auditMange"],
    // 功能权限
    "points": ["uploadImage", "userEdit", "articleEdit"]
  }
}

前端路由元数据

前端将页面权限分为两种,一种是公共模块,不需要做权限控制,所有人都可以访问,比如 404 页面,500 页面,个人信息页面等。另一种就是私有模块,根据用户权限的不同,展示不同的菜单和功能。

  1. 首先在router/index中定义两个变量,分别是publicRoutesprivateRoutes,并将其导出提供外界使用。
import { createRouter, createWebHistory } from "vue-router";
import UserManage from "./modules/UserManage.js";
import ArticleManage from "./modules/ArticleManage.js";
import AuditMange from "./modules/AuditMange.js";
import RoleManage from "./modules/RoleManage.js";

// 定义私有路由表,这个路由表总包含所有可变的菜单
// 后续通过后端提供的用户权限,从此路由路由表中筛选出来用户所能访问的菜单
// 将这个结果添加到总路由表中,构成最终的路由表
export const privateRoutes = [
  UserManage,
  RoleManage,
  ArticleManage,
  AuditMange,
];

// 定义公用路由表,在创建router的时候,直接加入到总路由表中
export const publicRoutes = [
  {
    // 登录页面
    path: "/login",
    name: "login",
    component: () =>
      import(/* webpackChunkName: "login" */ "@/views/login/index"),
  },
  {
    // 个人中心
    path: "/",
    name: "home",
    redirect: "/profile", // 进入首页自动展示个人中心页面
    component: layout,
    children: [
      {
        path: "/profile",
        name: "profile",
        component: () =>
          import(/* webpackChunkName: "profile" */ "@/views/profile/index"),
        meta: {
          title: "个人中心",
          icon: "el-icon-user",
        },
      },
      {
        // 404 页面
        path: "/404",
        name: "404",
        component: () =>
          import(/* webpackChunkName: "profile" */ "@/views/error-page/404"),
      },
    ],
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  // 将公用的路由表加入到总路由表中
  routes: publicRoutes,
});

export default router;
  1. 上面在创建privateRoutes的时候,导入了多个模块,这些模块可以单独的创建,以roleManage为例:
import layout from "@/layout";

export default {
  path: "/user",
  // 这里的重定向表示访问地址是 /user 的时候,重定向到 /user/manage
  // 如果 当前用户没有 /user/manage 访问权限,则就跳转到404页面
  redirect: "/user/manage",
  component: layout,
  // 此名称是对应后端权限表的权限名称
  name: "roleManage",
  meta: {
    title: "用户中心",
    icon: "personnel",
  },
  children: [
    {
      path: "/role/manage",
      name: "roleManage",
      component: () => import("@/views/role-manage/index"),
      meta: {
        title: "用户管理",
        icon: "role-manage",
      },
    },
  ],
};

到这里私有路由表的元数据已经创建完成,公有路由表已经加入到总路由表中。接下来就是根据后端返回的数据来做相应的处理。

动态路由表

用户登录后,从用户的用户信息中获取对应的权限,结合已有的私有路由表元数据,获取最终的用户私有路由表。

  1. store/moudles下创建一个permission.js文件,用来专门处理权限逻辑。
import { privateRoutes } from "@/router";

export default {
  // namespaced必须要写
  namespaced: true,
  state: () => ({}),
  mutations: {},
  actions: {
    filterRoutes(context, menus) {
      const routes = [];
      // 过滤获取最终用户私有权限路由表
      menus.forEach((key) => {
        routes.push(...privateRoutes.filter((item) => item.name === key));
      });
      // 最后添加 不匹配路由进入 404
      // 在公共路由里面定义了404页面,这里直接重新下进去即可
      routes.push({
        path: "/:catchAll(.*)",
        redirect: "/404",
      });
      return routes;
    },
  },
};
  1. 这里是将 store 内的内容,根据不同模块进行分类,创建不同的 js 文件,统一导入到 store 下的index.js文件,这里创建的permission.js同样要导入到index.js中。
import permission from "./modules/permission.js";

export default createStore({
  getters,
  modules: {
    permission,
  },
});
  1. 在路由的router.beforeEach方法中,将用户信息中的权限信息取出,并调用上面的filterRoutes方法,获取私有路由表结果。
import store from "@/store";

router.beforeEach(async (to, from, next) => {
  // 1. 用户已经登录,可以直接前往目标页面,不能再去login页面
  if (store.getters.token) {
    // 判断token是否已经失效
    if (!checkTokenAvailability()) {
      if (to.name === "login") {
        next("/");
      } else {
        // 这里需要获取用户权限信息
        if (!store.getters.hasUserInfo) {
          // 这一步是调用后端接口去获取用户信息
          const { permission } = await store.dispatch("user/getUserInfo");
          const userRoutes = await store.dispatch(
            "permission/filterRoutes",
            permission.menus
          );
          //循环遍历userRoutes,加入到总路由表中
          userRoutes.forEach((item) => router.addRoute(item));
          // 添加完动态路由后,需要主动重新跳转一次
          return next(to.path);
        }
        next();
      }
    } else {
      // 这里需要同时清理store和localStorage内缓存的用户数据
      store.commit("user/userLogout");
      next("/login");
    }
  } else {
    // 这里需要同时清理store和localStorage内缓存的用户数据(健壮性考虑)
    //store.commit('user/userLogout');
    // 表示前往的路径是在白名单内
    if (whiteList.indexOf(to.name) > -1) {
      next();
    } else {
      next("/login");
    }
  }
});

到这里菜单权限基本就可以了,但是存在几个个问题。

  1. 当用户登录后,如果权限发生变化,不会实时的响应,需要用户退出后重新登陆才行。(可以根据实际需求来进行修改,是否每次都刷新用户信息)
  2. 当用户退出后,需要清空store内缓存的数据,使用removeRoute方法来清理数据。

动态路由清理

在用户退出的时候,需要对当前退出用户的动态路由进行清理,否则会出现其他用户登录的时候,权限列表中混入之前登录用户的菜单权限。。清理主要分为两个部分,第一个部分是 store 中存储的用户信息,第二步分时 localStorage 中存储的。

  1. router/index文件中,编写一个resetRouter方法,用于清理 store 中存储的用户动态路由信息。
export const restRouter = () => {
  const menus = store.getters?.userInfo?.permission?.menus;
  if (menus) {
    menus.forEach((menu) => {
      // removeRoute方法的参数对应的是路由的名称
      // 这里menus数组中的值和动态路由里面的name是相对应的
      // 因此可以通过此方式直接删除动态路由信息
      router.removeRoute(menu);
    });
  }
};
  1. 在用户退出的逻辑中调用resetRouter方法。
import { resetRouter } from '@/router';

logout(){
  resetRouter();
  //……此处是删除localStorage中缓存的userInfo数据及其他缓存信息
}

动态指令

上面菜单权限搞定,这里需要实现页面内的具体功能权限。这个时候就需要使用到自定义指令。

  1. src下创建directives/permission.js文件,内容如下。
import store from '@/store'

function checkPermission(el,binding) {
  // 获取绑定的值,此处为权限,也就是标签中通过v-permision指定的值,是个数组
  const { value } =binding;
  // 获取用户的功能权限列表
  const points = store.getters.userInfo.permission.points;
  // 规定传入的权限必须是数组
  if(value && value instanceof Array) {
    // 匹配检查是否有权限
    // some函数会遍历points数组中的元素
    // 如果value.includes函数返回结果是true,则遍历终止,并最终返回true,否则返回false
    const hasPermission = points.some(point => {
      return value.includes(point)
    });
    // 如果无法匹配,表示对应的权限没有,则执行删除这个操作功能
    // 比如某个页面中的删除按钮功能,如果此人没有删除操作权限,则这个按钮的dom元素就会被删除,在页面中也就没有这个按钮了
    if(!hasPermission) {
      // 找到当前需要删除dom对象的父节点,通过父节点删除当前dom对象
      el.parentNode && el.parentNode.removeChild(el)
    }
  }else{
    throw new Error('v-permission value must be Array. e.g. ["per1","per2"]')
  }
},
export default {
    // 在绑定元素的父组件被挂载后调用
    mounted(el,binding) {
      checkPermission(el,binding);
    },
    // 在包含组件的vNode及其子组件的vNode更新后调用
    update(el,binding) {
      checkPermission(el,binding);
    }
}
  1. src/directives,目录下创建一个index.js文件,在此文件中引入permission.js文件。
import permission from "./permission.js";

export default (app) => {
  app.directive("permission", permission);
};
  1. main.js中导入directives并挂在到项目中。
import installDirectives from "@/directives";

const app = createApp(App);
installDirectives(app);
  1. 在代码中使用自定义指令,实现操作功能权限的控制。
<div>
  <el-button>修改</el-button>
  <!-- 当用户信息里面的points数据里有userDelete,表示有此功能,否者此标签将在渲染页面的时候被删除 -->
  <!-- 这种方式相当于v-if -->
  <el-button v-permission="['userDelete']">删除</el-button>
</div>

总结

权限控制主要分为两个部分,一个是菜单权限控制,一个是操作功能权限控制。菜单权限控制使用动态的路由来实现,操作功能权限通过自定义指令来实现。在大部分的项目中,这是一个固定的范式,也就是说可以作为一个模版使用。


文章作者: 程序猿洞晓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 程序猿洞晓 !
评论
 上一篇
Vue中集成MarkDown和富文本编辑器 Vue中集成MarkDown和富文本编辑器
后台管理系统经常需要提供内容的数据功能,在MarkDown没有普及的时候,基本都是使用富文本编辑器,本文将记录这两种编辑器的使用。
2023-05-05
下一篇 
Sharding-jdbc项目构建和基本本功能 Sharding-jdbc项目构建和基本本功能
数据在体量较大的时候,经常会涉及到分库分表,现在市面上目前使用较多的是基于代理的Mycat和基于客户端的sharding-jdbc,相对于可维护性、学习成本、部署成本等综合考虑,使用Sharding-jdbc是个不错的选择。
2023-04-19
  目录