实现思路
实现权限控制,主要是前后端两个部分,后端将具体的权限封装到数据库中,前端将请求所得的权限信息封装到动态路由中,用以控制菜单权限,但是在功能权限控制时,前端需要通过自定义指令控制。通过这一些列的操作,就能完成权限控制。实现用户权限控制主要需要解决以下问题:
- 数据库如何设计,数据库分为用户表、权限表、角色表、用户角色关联表、角色权限关联表,这也是用户权限控制固定的建表模式。
- 前端实现动态路由达到菜单权限控制。用户登录后,从后端获取当前用户所拥有的菜单权限列表,将其加入到动态路由中,最好的就是后端返回拿来即用,无需二次封装。
- 前端实现功能控制,前端封装自定义指令,通过判断当前功能是否在功能权限列表中,是的展示,不是的隐藏。当然功能权限列表也是通过后端封装。
后端表设计
精简化的数据库表设计。
用户表
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 页面,个人信息页面等。另一种就是私有模块,根据用户权限的不同,展示不同的菜单和功能。
- 首先在
router/index
中定义两个变量,分别是publicRoutes
和privateRoutes
,并将其导出提供外界使用。
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;
- 上面在创建
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",
},
},
],
};
到这里私有路由表的元数据已经创建完成,公有路由表已经加入到总路由表中。接下来就是根据后端返回的数据来做相应的处理。
动态路由表
用户登录后,从用户的用户信息中获取对应的权限,结合已有的私有路由表元数据,获取最终的用户私有路由表。
- 在
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;
},
},
};
- 这里是将 store 内的内容,根据不同模块进行分类,创建不同的 js 文件,统一导入到 store 下的
index.js
文件,这里创建的permission.js
同样要导入到index.js
中。
import permission from "./modules/permission.js";
export default createStore({
getters,
modules: {
permission,
},
});
- 在路由的
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");
}
}
});
到这里菜单权限基本就可以了,但是存在几个个问题。
- 当用户登录后,如果权限发生变化,不会实时的响应,需要用户退出后重新登陆才行。(可以根据实际需求来进行修改,是否每次都刷新用户信息)
- 当用户退出后,需要清空
store
内缓存的数据,使用removeRoute
方法来清理数据。
动态路由清理
在用户退出的时候,需要对当前退出用户的动态路由进行清理,否则会出现其他用户登录的时候,权限列表中混入之前登录用户的菜单权限。。清理主要分为两个部分,第一个部分是 store
中存储的用户信息,第二步分时 localStorage
中存储的。
- 在
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);
});
}
};
- 在用户退出的逻辑中调用
resetRouter
方法。
import { resetRouter } from '@/router';
logout(){
resetRouter();
//……此处是删除localStorage中缓存的userInfo数据及其他缓存信息
}
动态指令
上面菜单权限搞定,这里需要实现页面内的具体功能权限。这个时候就需要使用到自定义指令。
- 在
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);
}
}
- 在
src/directives
,目录下创建一个index.js
文件,在此文件中引入permission.js
文件。
import permission from "./permission.js";
export default (app) => {
app.directive("permission", permission);
};
- 在
main.js
中导入directives
并挂在到项目中。
import installDirectives from "@/directives";
const app = createApp(App);
installDirectives(app);
- 在代码中使用自定义指令,实现操作功能权限的控制。
<div>
<el-button>修改</el-button>
<!-- 当用户信息里面的points数据里有userDelete,表示有此功能,否者此标签将在渲染页面的时候被删除 -->
<!-- 这种方式相当于v-if -->
<el-button v-permission="['userDelete']">删除</el-button>
</div>
总结
权限控制主要分为两个部分,一个是菜单权限控制,一个是操作功能权限控制。菜单权限控制使用动态的路由来实现,操作功能权限通过自定义指令来实现。在大部分的项目中,这是一个固定的范式,也就是说可以作为一个模版使用。