前言
开发 个人主页前后端分离项目 已经结束了,想着对项目中的各个技术点进行总结下,这也是第一篇开始记录并分析技术点的文章,今天想说一说关于管理端权限的设计方案,也是项目中最为关键的部分。
主线思路
项目中我采用的权限设计方案属于基于角色的访问控制(Role-Based Access Control
,RBAC
)。RBAC
是一种常见且广泛使用的权限管理模型,它通过将用户分配到不同的角色,然后将角色与特定的权限关联,来实现对系统资源的访问控制。
基于RBAC
模型的权限设计方案有四个部分:
- 角色(Roles): 角色是一组用户,这些用户在系统中具有相似的权限需求。每个角色都会被分配一组权限,然后用户被分配到角色而不是直接分配权限。
- 权限(Permissions): 权限定义了用户可以执行的操作或访问的资源。每个角色都会被授予一组权限,这些权限定义了该角色所能执行的操作。
- 用户(Users): 用户是系统的最终操作者。每个用户都会被分配一个或多个角色(项目中没有使用多角色),这些角色决定了他们在系统中的权限。
- 前端路由匹配 : 前端根据用户的角色和权限信息,动态生成菜单和路由。这意味着当用户登录系统时,前端会根据用户的角色查询其对应的权限,然后根据权限配置来渲染出适当的菜单和页面。
在 RBAC
模型中,角色充当了用户和权限之间的中介,简化了权限管理和维护。
后端数据库设计
在上图中可以看到有张权限表里面数据都是各种操作接口的标识:用户查询操作 sys:users:list
、角色查询操作 sys:roles:list
、权限管理查询操作sys:permissions:list
。很容易看出权限基本关系是:当用户登录后再查询出角色信息,然后关联出角色对应的权限信息。
前端权限设计
原理:根据权限表数据和路由元信息进行匹配,然后筛选出对应的菜单
- 路由设计
例如 下面定义了用户管理、角色管理和权限管理的菜单路由,其中的meta
元信息中 perms
字段配置有该菜单的所有操作权限标识
[
{
path: '/dir-users-info',
name: 'dir-users-info',
meta: {
title: '用户管理',
icon: 'yonghuguanli',
requiresAuth: true,
perms: [
'sys:users:list',
'sys:users:create',
'sys:users:update',
'sys:users:delete',
],
},
component: () => import('@/views/sys/users/dir-users-info.vue'),
},
{
path: '/dir-roles-info',
name: 'dir-roles-info',
meta: {
title: '角色管理',
icon: 'yonghuguanli',
requiresAuth: true,
perms: [
'sys:roles:list',
'sys:roles:create',
'sys:roles:update',
'sys:roles:delete',
],
},
component: () => import('@/views/sys/users/dir-roles-info.vue'),
},
{
path: '/dir-permissions-info',
name: 'dir-permissions-info',
meta: {
title: '权限管理',
icon: 'yonghuguanli',
requiresAuth: true,
perms: [
'sys:permissions:list',
'sys:permissions:create',
'sys:permissions:update',
'sys:permissions:delete',
],
},
component: () => import('@/views/sys/users/dir-permissions-info.vue'),
}
]
- 检查路由对象是否具有权限
/**
* 检查路由对象是否具有权限
* @param {Array} perms - 权限列表
* @param {Object} route - 路由对象
* @returns {boolean} - 是否具有权限
*/
function hasPermission(perms, route) {
if (route.meta && route.meta.perms) {
// 如果路由对象定义了 meta 属性或者定义 meta.perms 属性,那么就根据权限值来判断是否具有权限
return perms.some(perm => route.meta.perms.includes(perm))
} else {
// 如果路由对象没有定义 meta 属性或者没有定义 meta.perms 属性,那么默认认为具有权限,返回 true。
return true
}
}
- 递归所有路由表根据权限列表筛选异步路由:
/**
* 根据权限列表筛选异步路由配置
* @param {Array} routes - 路由配置表
* @param {Array} perms - 权限列表
* @returns {Array} - 筛选后的路由配置
*/
function filterAsyncRouter(routes, perms) {
const res = []
routes.forEach(route => {
// 创建临时变量 tmp 可以在后续的操作中不会修改原始的路由对象。
const tmp = {...route}
if (!tmp.hidden && tmp.children) {
// 先对子路由进行深度筛选,确保子路由也符合权限要求
tmp.children = filterAsyncRouter(tmp.children, perms)
if (tmp.children && tmp.children.length > 0) {
res.push(tmp)
}
} else {
// 对于没有子路由的路由对象,直接进行权限判断
if (!tmp.hidden && hasPermission(perms, tmp)) {
res.push(tmp)
}
}
})
return res
}
- 生成出该用户具有权限的路由表:
export const menuList = function () {
const asyncRoutes = useRouter().options.routes[0].children.filter(e => !e.hidden)
//筛选路由表
const permissionList = dbUtils.get('perms');
if (!permissionList.length) {
// 清空所有缓存数据
dbUtils.clear()
// 重置路由重新登录
return useRouter().replace('/login')
}
let accessedRouters
if (permissionList.includes('*')) {
// 如果是超级管理员则无需权限验证
accessedRouters = asyncRoutes
} else {
accessedRouters = filterAsyncRouter(asyncRoutes, permissionList);
}
return accessedRouters
}
- 将生成好的路由表去渲染前端菜单。前端工作基本结束
后端权限设计
原理:是对每个请求进行权限验证 每个请求前端都会携带token,基于这个信息来进行验证用户的权限
其中会经过两轮的验证:token验证 和 接口权限验证 以确保数据的安全性
- 首先进行
token
验证
使用express-jwt
来进行token的身份验证
、然后存储用户信息和id到req对象中
/**
* Token 身份验证中间件
*
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
* @param {Function} next - 下一个中间件函数
* @returns {void}
* @throws {Error} - 如果身份验证失败,则抛出错误
*/
const {expressjwt: jwt} = require("express-jwt");
function tokenAuthentication(req, res, next) {
jwt({
secret: process.env.SIGN_KEY,
algorithms: ['HS256'],
requestProperty: 'user',
credentialsRequired: true,
getToken: function fromHeaderOrQuerystring(req, res) {
if (req.headers.authorization &&
req.headers.authorization.split(" ")[0] === "Bearer") {
return req.headers.authorization.split(" ")[1];
} else if (req.query && req.query.token) {
return req.query.token;
}
return null;
},
})(req, res, function (err) {
if (err) {
//抛出错误给全局错误信息处理
return next(err);
}
req.userId = req.user._id;
next();
});
}
module.exports = tokenAuthentication;
- 接口权限验证
将req对象中携带用户信息的user字段取出进行操作
// 验证接口权限 auth:'接口的预设权限'
// 示例:checkApiPermission('sys:users:list')
const checkApiPermission = (auth) => {
return async (req, res, next) => {
try {
const roleInfo = await RolesModel.findById(req.user.roleId)
if (!roleInfo) return log.error('该用户还未分配角色')
if (roleInfo) {
if (!roleInfo.status) {
apiResponse.unauthorizedResponse(res, '您的角色已被禁用,请联系管理员')
return false
}
// 对超级管理员或其他
if (roleInfo.perms.includes('*') || roleInfo.perms.includes(auth)) {
const permissionInfo = await PermissionsModel.findOne({key: auth})
// 权限已被禁用
if (!roleInfo.perms.includes('*') && !permissionInfo.status) {
return apiResponse.unauthorizedResponse(res, '您访问的权限已被禁用,请联系管理员')
}
// 接口验证通过,继续下一步中间件或处理程序
return next();
} else {
return apiResponse.unauthorizedResponse(res, '您暂时没有权限访问,请联系管理员')
}
}
} catch (err) {
return apiResponse.unauthorizedResponse(res, '接口权限验证错误')
}
};
};
总结
- 权限设计方案的缺点:
1.1 前端页面配置繁琐: 在新增带有权限的页面时,需要在路由表和权限管理中都进行配置。这可能会导致维护时的一些不便,特别是当功能模块较多时。
1.2 前端菜单自定义限制: 方案中的前端菜单配置可能受限于仅能控制菜单和用户的权限关系,而对于菜单的展示内容、样式、图标等方面的定制可能需要修改前端源代码,增加了定制化的复杂度。
1.3 前后端协调: 需要确保前后端之间预设的权限标识保持一致,以避免出现不一致的情况。这需要进行前后端的密切协调,以确保权限验证的一致性。 - 权限设计方案的优点:
2.1 灵活性和可扩展性: 当系统需要新增功能或进行权限调整时,只需更新角色的权限配置,而无需逐个调整用户的权限。这种方式使得权限的调整更为集中和高效,也降低了出错的可能性。
2.2 代码维护便捷: 将权限逻辑集中在一处,即权限管理部分,可以使代码更易于维护和理解。同时,权限的变更对其他部分的影响较小,降低了维护成本。
这种权限设计方案在实际应用中常用于需要管理多种角色和复杂权限控制的系统。它允许系统管理员根据不同的用户职能来配置权限,而不必关注每个用户的个体权限。这种集中式的权限管理模式有助于系统的可维护性和可扩展性。
然而,也可以在实际应用中根据具体情况进行一些调整,以平衡灵活性和管理成本。例如,对于前端的菜单和页面展示定制化,也可以考虑引入一些配置文件或者从后端动态获取信息,以减少前端源代码的修改。
不明白之处或者觉得处理的不好的地方可以评论区留言,期待和各位大佬的交流😊
基于vue3、nodejs、mongodb 个人主页前后端分离项目
https://gitee.com/Z568_568/ZHOUYI-Homepage.git