RBAC 权限控制:深入到按钮级别的实现
一、前端核心思路
1. 大致实现思路
后端都过SELECT连表查询把当前登录的用户对应所有的权限返回过来,前端把用户对应所有的权限 存起来to(vuex/pinia) 中 ,接着前端工程师需要知道每个按钮对应的权限代号如:
。至此为止,我们即知道了用户对应所有的权限也知道了每个权限的名字(或代号),现在我们可以从前端的层面区控制某个权限按钮的显示与否如:
<a-button
type="primary"
v-if="userPermiss_vuex.includes('user_add')"
@click="add_user()"
>添加用户</a-button
>
<!-- userPermiss_vuex:存储在vuex中的用户对应所有的权限 -->
2. 具体实现
首先在用户登录成功的时候调用获取用户所以权限的方法,方法如下:
// 获取用户对应的权限
const userPermiss = async () => {
try {
const results = await http.get("/api/users/userPermiss")
const userPermiss = results.data.results
// console.log('获取用户对应的权限::',results.data)
// 将用户权限存到vuex中
store.commit("SET_PERMISS", userPermiss);
} catch (error) {
console.log(error);
message.error(error.response.data.message);
}
};
后端运用到的数据表:
对应的后端接口代码:
// model/user.js
static async userPermiss(userId) {
try {
const sql = `
SELECT
Roles.id,
Roles.name,
Permissions.name as permission_name,
Permissions.permission_code
FROM UserRoles
JOIN Roles ON UserRoles.role_id = Roles.id
JOIN RolePermissions ON Roles.id = RolePermissions.role_id
JOIN Permissions ON RolePermissions.permission_id = Permissions.id
WHERE UserRoles.user_id = ?
`;
const roleRows = await query(sql, [userId]);
if (!roleRows || roleRows.length === 0) {
return []; // 返回空数组而不是undefined或null
}
const userPermissions = roleRows.map(role => role.permission_code);
return [...new Set(userPermissions)]; // 去重
} catch (error) {
console.error('获取用户权限失败:', error);
throw error; // 或者返回空数组: return [];
}
}
在具体页面中控制权限按钮的显示与隐藏:
方式一:传统的方式:
<a-button
type="primary"
v-if="userPermiss_vuex.includes('user_add')"
@click="add_user()"
>添加用户</a-button
>
<script setup>
import { useStore } from "vuex";
const store = useStore();
// 用户对应的权限名
const userPermiss_vuex = store.state.userPermiss;
</script>
方式二:自定义指令的方式(推荐):
vue中自定义指令问题:
<template>
<a-button
type="primary"
@click="add_user()"
>添加用户</a-button
>
<a-button
type="primary"
danger
v-checkpermission:foo.bar="'user_delete'"
@click="delete_user()"
>删除用户</a-button
>
</template>
<script setup>
// 创建一个自定义指令,来统一处理权限存在与否
const vCheckpermission = {
mounted: (el, binding) => {
if (!userPermiss_vuex.includes(binding.value)) {
// 移除元素
el.parentNode?.removeChild(el);
}
console.log(el, binding);
// el.classList.add("is-highlight");
},
};
</script>
二、后端核心思路
当然了,只在前端去实现权限的控制肯定是不安全的,还需要在后端请求接口的时候进行无权限阻止访问的操作,这里就需要运用到中间件了,下面只讲解了加入权限限制的代码。
1. 首先,我们创建一个中间件文件夹middleware
2. 接着,在middleware
下创建 js
文件 auth.js
,代码如下:
// 引入jsonwebtoken模块,用于处理JWT(JSON Web Token)相关操作
const jwt = require('jsonwebtoken');
// 定义一个名为auth的中间件函数,用于验证请求的认证信息
const auth = (req, res, next) => {
// 打印请求头中的Authorization字段,用于调试查看是否有token传递过来
console.log(req.header('Authorization'));
// 判断请求头中是否包含Authorization字段
if (!req.header('Authorization')) {
// 如果没有Authorization字段,返回400状态码,并提示缺少token
res.status(400).json({ message: '缺少请求头:token为空' });
} else {
// 如果有Authorization字段,去掉字段值前面的"Bearer ",提取出token
const token = req.header('Authorization').replace('Bearer ', '');
// 打印提取出的token,用于调试查看token的值
console.log('token:::', token);
try {
// 使用jsonwebtoken的verify方法验证token是否有效
// 第一个参数是token,第二个参数是签名密钥(这里写的是'39qw89r23890',实际项目中应使用环境变量等安全方式存储)
const decoded = jwt.verify(token, '39qw89r23890');
// 打印解码后的token内容,用于调试查看解码结果
console.log("decoded:::", decoded);
// 将解码后的用户信息存储到req.user中,方便后续的路由处理函数使用
req.user = decoded;
// 如果token验证通过,调用next函数,继续执行后续的中间件或路由处理函数
next();
} catch (error) {
// 如果token验证失败(例如token过期、被篡改等),捕获错误
// 返回400状态码,并提示认证失败
res.status(400).json({ message: 'Authentication failed' });
}
}
};
// 导出auth中间件,供其他模块使用
module.exports = auth;
这段代码实现了一个 JWT (JSON Web Token) 认证中间件,主要用于验证和保护 Node.js 后端 API 路由。以下是详细解析:
核心功能解析
1. 检查请求头中的 Token
console.log(req.header('Authorization')) // 调试输出
if(!req.header('Authorization')){
res.status(400).json({ message: '缺少请求头:token为空' });
}
• 作用:检查请求头是否包含 Authorization
字段
• 失败处理:如果不存在,返回 400
状态码和错误消息
• 安全提示:实际生产环境建议返回 401 Unauthorized
更符合 HTTP 规范
2. 提取并清理 Token
const token = req.header('Authorization').replace('Bearer ', '');
console.log('token:::',token) // 调试输出
• 处理逻辑:从 Authorization: Bearer <token>
格式中提取纯 Token
• 格式要求:前端需按标准格式发送 Token,例如:
GET /api/protected-route
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
3. JWT 验证
const decoded = jwt.verify(token, '39qw89r23890');
console.log("decoded:::",decoded)
req.user = decoded;
next();
• 验证过程:
• 使用密钥 '39qw89r23890'
解密 Token
• 验证 Token 的有效性(是否过期、是否被篡改)
• 成功处理:
• 将解码后的用户数据挂载到 req.user
(通常包含用户ID、角色等)
• 调用 next()
进入下一个中间件或路由处理器
• 密钥注意:生产环境应使用环境变量存储密钥,而非硬编码
4. 错误处理
catch (error) {
res.status(400).json({ message: 'Authentication failed' });
}
• 捕获场景:
• Token 过期(TokenExpiredError
)
• Token 被篡改(JsonWebTokenError
)
• 其他验证错误
• 改进建议:
res.status(401).json({
code: 'INVALID_TOKEN',
message: 'Token验证失败',
error: error.message // 可选:开发环境返回具体错误
});
完整工作流程
- 前端请求 → 携带
Authorization
头 - 中间件检查 → 无Token → 立即拒绝
- Token验证 → 无效 → 返回错误
- 验证通过 → 附加用户数据到
req.user
→ 放行到后续路由
安全注意事项
-
密钥管理:
// 推荐从环境变量读取 const decoded = jwt.verify(token, process.env.JWT_SECRET);
-
Token 存储:
• 前端应使用HttpOnly
Cookie 或localStorage
安全存储
• 避免在 URL 中传递 Token(会记录到日志) -
增强验证:
// 可选:检查Token是否在黑名单(如用户登出后) if (await isTokenRevoked(token)) { return res.status(401).json({ message: 'Token已失效' }); }
-
日志安全:
• 实际生产环境应移除console.log(token)
,避免敏感信息泄露
典型使用场景
const express = require('express');
const auth = require('./middleware/auth');
const app = express();
app.get('/protected', auth, (req, res) => {
// 只有通过认证的请求能进入这里
res.json({
data: '敏感数据',
user: req.user // 来自解码的Token信息
});
});
与其他中间件的协作
中间件 | 执行顺序 | 功能补充 |
---|---|---|
express.json() | 需在 auth 之前 | 解析请求体供后续使用 |
helmet | 在 auth 之前 | 增强HTTP头安全性 |
rate-limiter | 在 auth 之前 | 防止暴力破解 |
总结
功能模块 | 关键点 |
---|---|
Token检查 | 验证 Authorization 头是否存在 |
JWT解密 | 使用密钥验证Token有效性 |
数据传递 | 通过 req.user 共享用户信息 |
错误处理 | 拦截非法请求并返回标准错误 |
此中间件是保护API路由的基础安全层,确保只有携带有效Token的请求能访问受保护资源。
3. 然后,在middleware
下创建 js
文件 checkPermission.js
,代码如下:
// 引入数据库连接池模块,用于执行数据库查询操作
const pool = require('../db');
// 定义一个名为checkPermission的高阶函数,用于创建权限检查中间件
// requiredPermission参数表示需要检查的权限代码
const checkPermission = (requiredPermission) => {
// 返回一个异步中间件函数
return async (req, res, next) => {
try {
// 根据req.user.id查询用户信息
const [userRows] = await pool.query('SELECT * FROM Users WHERE id = ?', [req.user.id]);
// 如果用户不存在,返回404状态码
if (userRows.length === 0) {
return res.status(404).json({ message: 'User not found' });
}
// 查询用户所拥有的角色及其对应的权限
// 通过多表连接查询,获取用户的角色和权限信息
const [roleRows] = await pool.query(`
SELECT Roles.id, Roles.name, Permissions.name as permission_name, Permissions.permission_code
FROM UserRoles
JOIN Roles ON UserRoles.role_id = Roles.id
JOIN RolePermissions ON Roles.id = RolePermissions.role_id
JOIN Permissions ON RolePermissions.permission_id = Permissions.id
WHERE UserRoles.user_id = ?
`, [req.user.id]);
// 提取用户所拥有的权限代码
const userPermissions = roleRows.map(role => role.permission_code);
// 打印用户所拥有的权限代码,用于调试
console.log("userPermissions:::", userPermissions);
// 将用户权限存入res.locals,方便后续使用(注释掉了)
// res.locals.userPermissions = userPermissions;
// 检查用户是否拥有所需的权限
if (userPermissions.includes(requiredPermission)) {
// 如果用户拥有权限,调用next函数继续执行后续中间件或路由处理函数
return next();
} else {
// 如果用户没有权限,返回400状态码并提示拒绝访问
return res.status(400).json({ message: '拒绝访问:当前角色暂未有此权限' });
}
} catch (error) {
// 如果发生错误,返回500状态码并提示内部服务器错误
return res.status(500).json({ message: 'Internal server error' });
}
};
};
// 导出checkPermission函数,供其他模块使用
module.exports = checkPermission;
这段代码实现了一个 动态权限检查中间件,用于在 Node.js 后端中对 API 请求进行细粒度的权限控制。以下是详细解析:
核心功能解析
1. 中间件工厂模式
const checkPermission = (requiredPermission) => {
return async (req, res, next) => { /*...*/ }
}
• 作用:接收一个权限码参数(如 'user_delete'
),生成对应的权限检查中间件
• 灵活度:可以针对不同路由要求不同权限(例如:checkPermission('user_read')
和 checkPermission('admin_access')
)
2. 用户存在性验证
const [userRows] = await pool.query('SELECT * FROM Users WHERE id = ?', [req.user.id]);
if (userRows.length === 0) {
return res.status(404).json({ message: 'User not found' });
}
• 前置条件:要求 req.user.id
必须存在(通常由前面的 JWT 中间件设置)
• 安全设计:即使 Token 有效,也会二次确认用户是否存在
3. 多表联合查询权限
SELECT
Roles.id,
Roles.name,
Permissions.name as permission_name,
Permissions.permission_code
FROM UserRoles
JOIN Roles ON UserRoles.role_id = Roles.id
JOIN RolePermissions ON Roles.id = RolePermissions.role_id
JOIN Permissions ON RolePermissions.permission_id = Permissions.id
WHERE UserRoles.user_id = ?
• 查询逻辑:
- 通过
UserRoles
表找到用户关联的角色 - 通过
RolePermissions
表找到角色关联的权限 - 最终获取权限代码(
permission_code
)
• 数据结构:
// 查询结果示例
[{
id: 1,
name: '管理员',
permission_name: '删除用户',
permission_code: 'user_delete'
}]
4. 权限校验逻辑
const userPermissions = roleRows.map(role => role.permission_code);
if (userPermissions.includes(requiredPermission)) {
return next(); // 放行
} else {
return res.status(400).json({ message: '拒绝访问:当前角色暂未有此权限' });
}
• 校验方式:检查用户权限列表中是否包含所需权限码
• 改进建议:
• 使用 403 Forbidden
状态码更符合 HTTP 规范(400 表示客户端错误)
• 返回更详细的错误信息:
javascript res.status(403).json({ code: 'PERMISSION_DENIED', required: requiredPermission, owned: userPermissions });
5. 错误处理
catch (error) {
return res.status(500).json({ message: 'Internal server error' });
}
• 捕获范围:数据库查询错误、网络问题等
• 改进建议:
console.error('权限检查错误:', error);
res.status(500).json({
code: 'SERVER_ERROR',
message: '权限验证服务不可用'
});
完整工作流程
- 请求进入 → 携带已认证的用户信息(
req.user.id
) - 数据库查询 → 获取用户关联的所有权限
- 权限比对 → 检查是否包含所需权限
- 结果处理:
• 通过 → 调用next()
进入业务逻辑
• 拒绝 → 返回 403 错误
典型使用场景
const express = require('express');
const checkPermission = require('./middleware/checkPermission');
const app = express();
// 删除用户接口需要 user_delete 权限
app.delete('/users/:id',
checkPermission('user_delete'),
(req, res) => {
// 只有有权限的用户能执行到这里
res.json({ success: true });
}
);
安全增强建议
-
权限缓存:
// 使用Redis缓存用户权限,减少数据库查询 const cachedPerms = await cache.get(`user:${req.user.id}:perms`); if (cachedPerms) { return cachedPerms.includes(requiredPermission) ? next() : deny(); }
-
权限继承:
// 实现权限继承(如admin自动拥有所有权限) if (userRoles.some(role => role.is_admin)) { return next(); }
-
批量权限检查:
// 扩展支持多权限检查(需满足任意一个或全部) checkPermissions({ any: ['user_read', 'admin_access'], all: ['log_export'] })
与其他中间件的协作
执行顺序 | 中间件 | 功能 |
---|---|---|
1 | express.json() | 解析请求体 |
2 | auth (JWT验证) | 用户认证 |
3 | checkPermission | 权限控制 |
4 | 业务逻辑 | 处理实际请求 |
总结
功能模块 | 关键点 | 优化方向 |
---|---|---|
中间件工厂 | 动态生成权限检查器 | 支持更复杂权限逻辑 |
数据库查询 | 多表联合查询权限 | 添加缓存机制 |
权限校验 | 简单包含检查 | 支持权限继承/组合 |
错误处理 | 基础错误捕获 | 更精细的错误分类 |
此中间件实现了 RBAC (基于角色的访问控制) 的核心功能,是保护敏感 API 的关键安全层。
4. 最后,在接口配置中增加上面两个中间件
// router/user.js
const express = require("express")
const router = express.Router()
// 引入控制层
const User = require("../controllers/users")
// 引入中间件
const checkPermission = require('../middleware/checkPermission');
const auth = require('../middleware/auth');
// User.create这个方法就是添加用户的,所以及对应的权限就是user_add。
// 因此就把user_add传给中间件checkPermission,让他进行判断当前用户是否有此权限
router.post('/create', auth, checkPermission('user_add'), User.create)
router.post('/delet', auth, checkPermission('user_delete'), User.delet)
router.post('/edit', auth, checkPermission('user_edit'), User.edit)
// 等等....
// 导出
module.exports = route
三、出现的问题(BUG)
我在后续的测试中,发现两个问题:
-
在退出一个账号后登录另一个账号的时候,先显示的是上一个用户的权限?
次要原因:这是因为你没有清除vuex中的数据
主要原因:是你在向vuex中存储数据出了问题
-
在登陆成功的时候用户的所有权限没有及时存储到vuex中,就直接跳转到主页了,这就导致先显示的是你上一个用户存储的权限/如果你退出时清除了vuex中的数据 显示的就是没有权限,此时必须要重新刷新一下才能恢复正常
主要原因:是你在向vuex中存储数据出了问题
您应该 先获取用户权限 (userPermiss()
),再跳转页面 (router.push()
),否则可能会导致权限未正确加载的问题。
问题分析
-
权限未加载就跳转:
• 如果先跳转页面,再异步获取权限,可能会导致:
◦ 页面已经渲染,但权限未加载完成,导致权限指令(如vCheckpermission
)无法正确判断权限。
◦ 动态路由可能依赖权限数据,如果权限未加载,动态路由可能不会正确生成。 -
动态路由依赖权限:
• 您的generateDynamicRoutes()
可能依赖userPermiss
数据,如果权限未加载,动态路由可能不会正确生成,导致页面访问异常。
修正后的代码
const submitForm = async () => {
try {
await formRef.value?.validate();
const response = await axios.post("/api/users/login", {
username: ruleForm.value.usermobile,
password: ruleForm.value.userpwd,
});
message.info(response.data.msg);
const token = response.data.data.token;
const userid = response.data.data.userInfo.id;
const username = response.data.data.userInfo.username;
const roleid = response.data.data.userInfo.role_id;
const role_code: string = response.data.data.userInfo.role_code;
localStorage.setItem("token", token);
localStorage.setItem("userid", userid);
localStorage.setItem("username", username);
localStorage.setItem("roleid", roleid);
localStorage.setItem("role_code", role_code);
localStorage.setItem("token_is_exp", "0");
let role_code_arr = ref<string[]>([]);
role_code_arr.value.push(role_code);
store.commit("SET_ROLES", role_code_arr);
// 1. 先获取权限(确保权限加载完成)
await userPermiss(); // 使用 await 确保权限加载完成
// 2. 生成动态路由(依赖权限数据)
await generateDynamicRoutes(); // 如果 generateDynamicRoutes 是异步的,也要 await
// 3. 最后跳转页面
if (token) {
await router.push("admin/user"); // 使用 await 确保跳转完成
}
console.log(token);
} catch (error: any) {
console.error("请求失败:", error);
message.error(error.response.data.msg);
}
};
关键改进
-
await userPermiss()
• 确保权限加载完成后再执行后续操作,避免权限未加载导致的权限指令失效问题。 -
await generateDynamicRoutes()
• 如果generateDynamicRoutes()
是异步的(例如依赖 API 请求),也要await
确保动态路由生成完成。 -
await router.push()
• 使用await
确保页面跳转完成,避免跳转过程中权限数据未完全加载。
额外优化建议
-
在
userPermiss()
里返回 Promise
• 确保userPermiss()
返回Promise
,以便await
能正确等待其完成:const userPermiss = async () => { try { const results = await http.get("/api/users/userPermiss"); const userPermiss = results.data.results; store.commit("SET_PERMISS", userPermiss); return userPermiss; // 返回数据,方便后续处理 } catch (error) { console.log(error); message.error(error.response.data.message); throw error; // 抛出错误,让外层 catch 捕获 } };
-
在
generateDynamicRoutes()
里也依赖userPermiss
• 如果动态路由依赖权限数据,可以确保权限加载完成后再生成路由:const generateDynamicRoutes = async () => { const permiss = store.state.userPermiss; if (!permiss.length) { await userPermiss(); // 如果权限未加载,先加载权限 } // 生成动态路由的逻辑... };
总结
✅ 正确顺序:
- 登录成功 → 存储 token 和用户信息
await userPermiss()
(确保权限加载完成)await generateDynamicRoutes()
(确保动态路由生成)await router.push()
(跳转页面)
这样可以避免权限未加载导致的页面权限控制失效或动态路由生成异常问题。