大部分管理系统都区分了很多模块,因各部门之间分工协作展示内容和所用功能都不同,所以一个健壮的后台管理项目离不开权限管理,本文旨在node小白也可以通过express+mysql搭建一个简单的用户权限管理系统。
设计思路为:
- 创建用户管理模块进行账号的管理以及角色分配
- 创建权限管理模块进行角色的管理以及角色权限的分配
- 登录校验成功之后查询用户信息存储用户角色和权限信息
- 根据用户权限分配路由和操作功能
要实现权限管理我们至少需要:用户表(users)、角色表(roles)、权限表(permissions),而用户可以有多个角色,每个角色又可以有多个权限,这里为多对对的关系,所以这里还需要:用户角色联结表(user_role)、角色权限联结表(role_permissions)。
关联关系如下:
表设计
/* 用户表 */
CREATE TABLE `user` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`account` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '账号',
`password` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '密码',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '用户名',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`status` tinyint NULL DEFAULT 0 COMMENT '状态:0 正常,1 关闭',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`memo` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb3 COLLATE=utf8_general_ci
COMMENT='用户表'
AUTO_INCREMENT=32
ROW_FORMAT=DYNAMIC
AVG_ROW_LENGTH=1638;
/* 用户角色联结表 */
CREATE TABLE `user_role_table` (
`id` int NOT NULL AUTO_INCREMENT,
`uid` int NULL COMMENT '用户id',
`rid` int NULL COMMENT '角色id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb3 COLLATE=utf8_general_ci
COMMENT='用户角色联结表'
AUTO_INCREMENT=29
ROW_FORMAT=DYNAMIC
AVG_ROW_LENGTH=1170;
/* 角色表 */
CREATE TABLE `role_table` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`role_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '角色类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb3 COLLATE=utf8_general_ci
COMMENT='角色表'
AUTO_INCREMENT=20
ROW_FORMAT=DYNAMIC
AVG_ROW_LENGTH=2730;
/* 角色权限联结表 */
CREATE TABLE `role_permissions` (
`id` int NOT NULL AUTO_INCREMENT,
`rid` int NULL COMMENT '角色id',
`pid` int NULL COMMENT '权限id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb3 COLLATE=utf8_general_ci
COMMENT='角色权限联结表'
AUTO_INCREMENT=49
ROW_FORMAT=DYNAMIC
AVG_ROW_LENGTH=442;
/* 权限表 */
CREATE TABLE `permissions` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '权限名称',
`type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '权限类型',
`class` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '分组类型',
`class_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '分组名称',
`isParent` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT 'true:父级,false:子级',
`disabled` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '是否禁用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb3 COLLATE=utf8_general_ci
COMMENT='权限表'
AUTO_INCREMENT=36
ROW_FORMAT=DYNAMIC
AVG_ROW_LENGTH=481;
用户管理
这里就以最简单的账号密码登录为例(具体登录流程在我的另一篇博客有详细介绍),一个典型的管理系统分为左侧路由列表和右侧功能展示区,我们可以根据不同的用户分配不同的角色权限来对左侧路由区域进行控制,以实现不同的用户展示不同的功能,要进行权限管理首先我们得有一个用户管理模块用来对用户进行增删改查和角色分配。
很多前端开发者和node小白可能对sql不是很熟练,这里推荐使用Sequelize进行数据库操作
Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能。
获取用户列表
用户角色联结表通过外键uid、rid将用户表和角色表关联,这里我采用多对多belongsToMany进行关联。
// 获取用户列表
async get_users_list(req, res, next) {
User.belongsToMany(RoleTable, { through: UserRoleTable, foreignKey: "uid", otherKey: "rid" });
try {
const result = await User.findAll({
include: {
model: RoleTable,
through: {
attributes: [],
},
},
});
req.resultData = getResultData(req, result);
next();
} catch (err) {
console.log("err: ", err);
res.send(sqlError);
}
},
插入用户信息中间件
首先校验数据库用户是否存在,之后进行用户创建和角色分配
/**
* 插入用户信息
* @param {Object} param {账号,密码,昵称,备注,状态,权限}
* @returns
*/
async insert_user_info(req, res, next) {
const { account, password, name, memo, status, role = [] } = req.body;
if (!account || !password || !name) {
res.send({
success: false,
errorMessage: "创建用户失败(请求参数缺失)",
});
return;
}
try {
// 校验用户是否存在
const count = await User.count({
where: { account },
});
if (count > 0) {
res.send({
success: false,
errorMessage: "用户已存在",
});
return;
}
// 创建用户
const user = await User.create({
account,
password: encrypt(password),
name,
memo: memo || null,
status: status ? 0 : 1,
});
// 添加权限
if (role.length > 0) {
await UserRoleTable.bulkCreate(
role.map((v) => {
return { uid: user.id, rid: v };
})
);
}
req.resultData = getResultData(req, true);
next();
} catch (err) {
console.log("err: ", err);
res.send(sqlError);
}
},
编辑用户信息中间件
这里分为基础信息变动和角色信息变动,基础信息变动直接通过id匹配update即可,而角色信息变动相对复杂。
角色信息编辑主要是通过操作用户角色联结表,我这里采用的是生成变动模型的方式,即先从数据库查询原始数据和传入数据进行对比生成变动部分的操作模型的方式,此方法胜在逻辑简单,缺点在于性能较差只适用于少量数据变动。
/**
* 更新用户信息
* @param {Object} param {用户id,账号,密码,昵称,备注,状态,权限}
* @returns
*/
async update_user_info(req, res, next) {
const { userId, account, password, name, memo, status, role = [] } = req.body;
if (!userId || !account || !password || !name) {
res.send({
success: false,
errorMessage: "创建用户失败(请求参数缺失)",
});
return;
}
try {
// 更新用户基础数据
await User.update(
{ name, account, password: encrypt(password), memo, status: status ? 0 : 1 },
{
where: { id: userId },
}
);
// 获取用户角色列表
const userRole = await UserRoleTable.findAll({ where: { uid: userId } });
// 变动模型
const variation = role
.map((v, i) => {
if (!userRole[i]) {
return { type: "create", uid: userId, rid: v }; // 未存在执行创建
} else if (userRole[i].uid === userId && userRole[i].rid === v) {
return { type: "skip" }; // 已存在执行跳过
} else if (userRole[i].uid === userId && userRole[i].rid !== v) {
return { type: "update", rid: v, id: userRole[i].id }; // 角色不同执行更新
}
})
.concat(
userRole.splice(role.length).map((v) => {
return { type: "destroy", id: v.id };
})
);
// 更新用户角色
await Promise.all(
variation.map((v) => {
if (v.type === "create") {
return UserRoleTable.create({ uid: v.uid, rid: v.rid });
} else if (v.type === "skip") {
return Promise.resolve(true);
} else if (v.type === "update") {
return UserRoleTable.update({ rid: v.rid }, { where: { id: v.id } });
} else if (v.type === "destroy") {
return UserRoleTable.destroy({ where: { id: v.id } });
}
})
);
req.resultData = getResultData(req, true);
next();
} catch (err) {
console.log("err: ", err);
res.send(sqlError);
}
},
权限管理
权限管理模块我采用的是左右布局和树形结构进行展示,此模块可对角色进行增删改,以及角色权限的分配。
获取角色权限列表
角色权限联结表通过外键rid、pid将角色表和权限表关联,这里采用多对多belongsToMany进行关联。
通过表关联查询出当前用户角色权限rolePermissions
这里class采用层级关系以逗号分隔,isParent标识顶级父元素,以isParent标识为起点通过递归生成树形结构数据。
获取角色权限列表中间件
/**
* 获取角色权限列表
* @param {Object} param {roleId:角色id}
*/
async get_role_permissions_list(req, res, next) {
const { roleId } = req.query;
RoleTable.belongsToMany(Permissions, {
through: RolePermissions,
foreignKey: "rid",
otherKey: "pid",
});
try {
// 获取当前角色权限
const result = await RoleTable.findAll({
include: {
model: Permissions,
through: {
attributes: [],
},
},
where: {
id: roleId,
},
});
const rolePermissions = result[0].permissions;
const permissions = JSON.parse(JSON.stringify(await Permissions.findAll()));
const permissionsTree = JSON.parse(
JSON.stringify(permissions.filter((v) => v.isParent === "true"))
);
const fn = (array) => {
array.forEach((v, i) => {
const children = permissions.filter((v1) => {
const ls = v1.class.split(",");
return v.class === ls.splice(0, ls.length - 1).join(",");
});
if (children.length > 0) fn(children);
v.children = children;
});
};
fn(permissionsTree);
req.resultData = getResultData(req, { treeData: permissionsTree, rolePermissions });
next();
} catch (err) {
console.log("err: ", err);
res.send(sqlError);
}
},
编辑角色权限列表中间件
在每一个节点编辑的时候请求接口变动,在角色权限联结表里增删对应的关联即可。
/**
* 编辑角色权限列表
* @param {Object} param {checked:[true(添加权限),false(删除权限)],pid:权限id,rid:角色id,id:权限树id}
*/
async edit_role_permissions(req, res, next) {
const { checked, pid, rid } = req.body;
try {
if (checked) {
await RolePermissions.create({ pid, rid });
} else {
await RolePermissions.destroy({ where: { pid, rid } });
}
req.resultData = getResultData(req, true);
next();
} catch (err) {
console.log("err: ", err);
res.send(sqlError);
}
},