Vue3 + Node.js RBAC 权限系统实战:4张表设计实现前后端动态路由与按钮级控制

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 });

这种设计的关键优势在于:

  1. 角色继承:通过角色的父子关系实现权限继承
  2. 菜单层级:支持无限级菜单嵌套
  3. 细粒度控制:权限码(code)对应前端按钮指令
  4. 高效查询: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. 权限验证流程全解析

完整的权限验证流程包含以下关键步骤:

  1. 登录认证

    • 用户提交凭证
    • 后端验证并返回JWT
    • 前端存储token并获取用户权限
  2. 路由守卫

    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'); } } });
  3. 菜单渲染

    • 根据权限过滤可访问路由
    • 递归生成侧边栏菜单
    • 动态注册路由实例
  4. API请求拦截

    service.interceptors.request.use(config => { const userStore = useUserStore(); if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}`; } return config; });
  5. 按钮控制

    • 自定义指令隐藏无权限按钮
    • 表格操作列动态渲染

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 细粒度权限控制进阶

对于更复杂的权限场景,我们可以实现:

  1. 数据权限控制

    // 在API请求中添加数据权限参数 export const getUsers = (params) => { const userStore = useUserStore(); return request({ url: '/users', params: { ...params, dataScope: userStore.dataScope // 如 'SELF', 'DEPT', 'ALL' } }); };
  2. 权限变更实时通知

    // 后端使用WebSocket通知权限变更 socket.on('permission-update', (userId) => { if (userStore.userInfo._id === userId) { userStore.getUserInfo(); // 重新获取权限 } });

7. 安全最佳实践

  1. JWT安全

    • 设置合理的过期时间(建议2-4小时)
    • 使用HTTPS传输
    • 实现token刷新机制
  2. 接口防护

    • 所有API默认拒绝访问
    • 细粒度的权限注解
    • 防SQL注入处理
  3. 前端防护

    • 敏感数据不存储在客户端
    • 路由级和组件级双重防护
    • 生产环境禁用开发者工具
// 示例:防止开发者工具打开 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 + NginxPM2集群

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. 项目扩展方向

  1. 多租户支持

    • 在角色模型中添加tenantId字段
    • 实现数据隔离中间件
  2. 权限模板

    • 预定义角色权限模板
    • 一键应用模板配置
  3. 操作审计

    • 记录关键操作日志
    • 实现操作回放功能
  4. 移动端适配

    • 响应式布局
    • 移动端专属菜单
// 操作审计日志示例 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 } });