Vue3 + Node.js RBAC 权限系统实战:4张表设计实现前后端动态路由与按钮级控制
Vue3 + Node.js RBAC 权限系统实战:从零构建企业级动态路由与按钮级控制
1. 现代权限系统的核心挑战与解决方案
在当今复杂的企业应用环境中,权限管理早已超越了简单的"谁能访问什么"的基础需求。现代系统需要面对:
- 动态路由管理:不同角色看到的导航菜单完全不同
- 细粒度控制:精确到按钮级别的权限验证
- 实时生效:权限变更无需重新登录立即生效
- 前后端协同:完整的安全链条从接口到UI
RBAC(基于角色的访问控制)模型通过引入角色这一中间层,完美解决了这些挑战。我们的实战方案将基于以下技术栈:
前端技术栈:
- Vue3 + TypeScript
- Pinia 状态管理
- Vue Router 动态路由
- Element Plus UI组件库
后端技术栈:
- Node.js + Express
- MongoDB + Mongoose
- JWT 认证
2. 数据库设计:精简而强大的4表模型
我们采用经典的RBAC四表设计,通过Mongoose Schema定义如下:
// 用户模型 const userSchema = new Schema({ username: { type: String, unique: true, required: true }, password: { type: String, required: true }, roles: [{ type: Schema.Types.ObjectId, ref: 'Role' }], status: { type: Boolean, default: true } }, { timestamps: true }); // 角色模型 const roleSchema = new Schema({ name: { type: String, unique: true, required: true }, desc: String, menus: [{ type: Schema.Types.ObjectId, ref: 'Menu' }], permissions: [{ type: Schema.Types.ObjectId, ref: 'Permission' }] }); // 菜单模型(对应前端路由) const menuSchema = new Schema({ path: { type: String, required: true }, component: { type: String, required: true }, name: { type: String, required: true, unique: true }, meta: { title: { type: String, required: true }, icon: String, hidden: { type: Boolean, default: false } }, parent: { type: Schema.Types.ObjectId, ref: 'Menu', default: null }, order: { type: Number, default: 0 } }); // 权限模型(按钮级控制) const permissionSchema = new Schema({ name: { type: String, required: true, unique: true }, code: { type: String, required: true, unique: true }, // 如 'user:create' desc: String });这种设计的关键优势在于:
- 角色继承:通过角色的父子关系实现权限继承
- 菜单层级:支持无限级菜单嵌套
- 细粒度控制:权限码(code)对应前端按钮指令
- 高效查询:Mongoose的populate方法轻松获取关联数据
3. 后端核心实现:权限验证与动态路由API
3.1 JWT认证中间件
const jwtAuth = async (req, res, next) => { const token = req.header('Authorization')?.replace('Bearer ', ''); if (!token) return res.status(401).send('请先登录'); try { const decoded = jwt.verify(token, process.env.JWT_SECRET); const user = await User.findById(decoded.userId).populate('roles'); if (!user) throw new Error(); req.user = user; req.token = token; next(); } catch (err) { res.status(401).send('认证失败'); } };3.2 动态路由API端点
router.get('/user/routes', jwtAuth, async (req, res) => { const user = req.user; // 获取用户所有角色的菜单 const roles = await Role.find({ _id: { $in: user.roles } }) .populate('menus') .populate('permissions'); // 合并去重菜单 const menus = [...new Set(roles.flatMap(role => role.menus))]; // 构建树形结构 const buildMenuTree = (menus, parentId = null) => { return menus .filter(menu => menu.parent?.toString() === parentId?.toString()) .map(menu => ({ path: menu.path, component: menu.component, name: menu.name, meta: menu.meta, children: buildMenuTree(menus, menu._id) })); }; const routes = buildMenuTree(menus); // 返回路由和权限码 res.send({ routes, permissions: [...new Set(roles.flatMap(role => role.permissions.map(p => p.code)))] }); });3.3 权限验证中间件
const hasPermission = (requiredPermission) => { return async (req, res, next) => { const user = req.user; const roles = await Role.find({ _id: { $in: user.roles } }) .populate('permissions'); const hasPerm = roles.some(role => role.permissions.some(p => p.code === requiredPermission) ); if (!hasPerm) { return res.status(403).send('无权访问'); } next(); }; }; // 使用示例 router.post('/users', jwtAuth, hasPermission('user:create'), userController.create);4. 前端动态路由实现
4.1 路由定义与分类
// 静态路由(所有用户可见) export const constantRoutes = [ { path: '/login', component: () => import('@/views/Login.vue'), hidden: true }, { path: '/404', component: () => import('@/views/404.vue'), hidden: true } ]; // 异步路由(需要动态加载) export const asyncRoutes = [ { path: '/system', component: Layout, meta: { title: '系统管理', icon: 'setting' }, children: [ { path: 'user', component: () => import('@/views/system/user/index.vue'), name: 'User', meta: { title: '用户管理', permission: 'system:user' } } ] } ];4.2 Pinia存储用户权限状态
export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token'), userInfo: null, routes: [], permissions: [] }), actions: { async getUserInfo() { const res = await getUserRoutes(); this.routes = res.routes; this.permissions = res.permissions; // 动态添加路由 res.routes.forEach(route => { router.addRoute(route); }); // 添加404路由 router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' }); }, async login(loginForm) { const res = await userLogin(loginForm); this.token = res.token; localStorage.setItem('token', res.token); await this.getUserInfo(); } } });4.3 按钮级权限指令
export const setupPermissionDirective = (app) => { app.directive('permission', { mounted(el, binding) { const { value } = binding; const userStore = useUserStore(); if (value && !userStore.permissions.includes(value)) { el.parentNode?.removeChild(el); } } }); }; // 使用示例 <el-button v-permission="'user:create'">新增用户</el-button>5. 权限验证流程全解析
完整的权限验证流程包含以下关键步骤:
登录认证:
- 用户提交凭证
- 后端验证并返回JWT
- 前端存储token并获取用户权限
路由守卫:
router.beforeEach(async (to, from, next) => { const userStore = useUserStore(); if (userStore.token) { if (to.path === '/login') { next('/'); } else { if (!userStore.userInfo) { try { await userStore.getUserInfo(); next({ ...to, replace: true }); } catch (error) { next('/login'); } } else { next(); } } } else { if (whiteList.includes(to.path)) { next(); } else { next('/login'); } } });菜单渲染:
- 根据权限过滤可访问路由
- 递归生成侧边栏菜单
- 动态注册路由实例
API请求拦截:
service.interceptors.request.use(config => { const userStore = useUserStore(); if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}`; } return config; });按钮控制:
- 自定义指令隐藏无权限按钮
- 表格操作列动态渲染
6. 高级技巧与性能优化
6.1 路由懒加载优化
// 使用Vite的import.meta.glob实现更精细的懒加载 const modules = import.meta.glob('../views/**/*.vue'); const asyncRoutes = [ { path: '/system', component: Layout, children: [ { path: 'user', component: modules['../views/system/user/index.vue'], name: 'User' } ] } ];6.2 权限缓存策略
// 使用localStorage缓存权限数据 const PERMISSION_KEY = 'permission_cache'; export const useUserStore = defineStore('user', { actions: { async getUserInfo() { // 尝试从缓存读取 const cache = localStorage.getItem(PERMISSION_KEY); if (cache) { const { routes, permissions, expires } = JSON.parse(cache); if (new Date(expires) > new Date()) { this.routes = routes; this.permissions = permissions; return; } } // 从API获取 const res = await getUserRoutes(); this.routes = res.routes; this.permissions = res.permissions; // 设置缓存(1小时有效) localStorage.setItem(PERMISSION_KEY, JSON.stringify({ routes: res.routes, permissions: res.permissions, expires: new Date(Date.now() + 3600000).toISOString() })); } } });6.3 细粒度权限控制进阶
对于更复杂的权限场景,我们可以实现:
数据权限控制:
// 在API请求中添加数据权限参数 export const getUsers = (params) => { const userStore = useUserStore(); return request({ url: '/users', params: { ...params, dataScope: userStore.dataScope // 如 'SELF', 'DEPT', 'ALL' } }); };权限变更实时通知:
// 后端使用WebSocket通知权限变更 socket.on('permission-update', (userId) => { if (userStore.userInfo._id === userId) { userStore.getUserInfo(); // 重新获取权限 } });
7. 安全最佳实践
JWT安全:
- 设置合理的过期时间(建议2-4小时)
- 使用HTTPS传输
- 实现token刷新机制
接口防护:
- 所有API默认拒绝访问
- 细粒度的权限注解
- 防SQL注入处理
前端防护:
- 敏感数据不存储在客户端
- 路由级和组件级双重防护
- 生产环境禁用开发者工具
// 示例:防止开发者工具打开 window.addEventListener('keydown', (e) => { if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && e.key === 'I')) { e.preventDefault(); return false; } });8. 部署与监控
8.1 生产环境部署建议
| 环境 | 前端部署 | 后端部署 |
|---|---|---|
| 开发 | Vite开发服务器 | nodemon热重载 |
| 测试 | Docker容器 | Docker容器 |
| 生产 | CDN + Nginx | PM2集群 |
8.2 性能监控配置
// 使用PM2监控Node.js应用 module.exports = { apps: [{ name: 'rbac-api', script: 'app.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000 }, max_memory_restart: '1G', error_file: './logs/error.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss' }] };9. 常见问题解决方案
Q1: 动态路由刷新后404?
解决方案:
// nginx配置 location / { try_files $uri $uri/ /index.html; }Q2: 权限变更需要重新登录?
解决方案:
// 在权限指令中添加响应式更新 app.directive('permission', { updated(el, binding) { const { value } = binding; const userStore = useUserStore(); if (value && !userStore.permissions.includes(value)) { el.style.display = 'none'; } else { el.style.display = ''; } } });Q3: 菜单图标不显示?
解决方案:
// 使用动态图标组件 const Icon = defineComponent({ props: { name: { type: String, required: true } }, setup(props) { return () => h(resolveComponent(`el-icon-${props.name}`)); } });10. 项目扩展方向
多租户支持:
- 在角色模型中添加tenantId字段
- 实现数据隔离中间件
权限模板:
- 预定义角色权限模板
- 一键应用模板配置
操作审计:
- 记录关键操作日志
- 实现操作回放功能
移动端适配:
- 响应式布局
- 移动端专属菜单
// 操作审计日志示例 const auditLog = new Schema({ userId: { type: Schema.Types.ObjectId, ref: 'User' }, action: String, entity: String, entityId: Schema.Types.ObjectId, ip: String, metadata: Schema.Types.Mixed, createdAt: { type: Date, default: Date.now } });