Node.js企业级RBAC权限模型:基于角色与声明的访问控制落地
在开发具备团队协同、多租户(Multi-Tenancy)或后台管理功能的全栈 Web 应用时,安全严密的**访问权限控制(Access Control)**是系统的核心中枢。
很多开发者在项目早期为了省事,直接在每个接口里写:
// ❌ 脆弱且难以维护的硬编码权限校验 if (user.role !== 'admin' && user.role !== 'super_manager') { return reply.status(403).send('无权访问'); }随着系统迭代,新增了“只读审计员”、“团队财务主管”、“部门组长”等细分角色,原先硬编码的代码就会迅速陷入灾难:修改一个角色的权限范围需要全量搜索修改几十处 Controller 文件,且极其容易产生权限泄露漏洞。
为了实现权限的“动态可配、细粒度控制与声明式鉴权”,我们在 Node.js (Fastify / Express) 中搭建了一套基于RBAC(基于角色的访问控制) + ABAC(基于属性的细粒度声明)的现代化权限中间件。
权限体系模型设计:用户 - 角色 - 权限点
[ 用户 (User) ] ──(拥有)──► [ 角色 (Role: 如 TeamAdmin, Editor) ] │ ▼ (绑定细粒度权限列表) [ 权限点 (Permissions) ] ├── 'report:create' (创建周报) ├── 'report:export' (导出PDF/长图) ├── 'team:member:invite' (邀请成员) └── 'billing:view' (查看财务账单)核心实现一:权限点常量定义与角色映射表
// src/auth/permissions.ts export const PERMISSIONS = { REPORT_CREATE: 'report:create', REPORT_READ: 'report:read', REPORT_EDIT_OWN: 'report:edit:own', REPORT_EDIT_ALL: 'report:edit:all', REPORT_DELETE: 'report:delete', BILLING_MANAGE: 'billing:manage', USER_INVITE: 'user:invite' } as const; export type Permission = typeof PERMISSIONS[keyof typeof PERMISSIONS]; export const ROLE_PERMISSIONS: Record<string, Permission[]> = { free_user: [ PERMISSIONS.REPORT_CREATE, PERMISSIONS.REPORT_READ, PERMISSIONS.REPORT_EDIT_OWN ], pro_user: [ PERMISSIONS.REPORT_CREATE, PERMISSIONS.REPORT_READ, PERMISSIONS.REPORT_EDIT_OWN, PERMISSIONS.REPORT_DELETE ], team_admin: [ PERMISSIONS.REPORT_CREATE, PERMISSIONS.REPORT_READ, PERMISSIONS.REPORT_EDIT_ALL, PERMISSIONS.REPORT_DELETE, PERMISSIONS.BILLING_MANAGE, PERMISSIONS.USER_INVITE ] };核心实现二:高阶声明式鉴权中间件工厂
编写一个通用且类型安全的中间件拦截器,支持在路由定义阶段直接声明所需权限:
// src/middlewares/requirePermission.ts import { FastifyRequest, FastifyReply } from 'fastify'; import { Permission, ROLE_PERMISSIONS } from '../auth/permissions'; export function requirePermission(requiredPermission: Permission) { return async (req: FastifyRequest, reply: FastifyReply) => { const user = (req as any).user; if (!user) { return reply.status(401).send({ code: 'UNAUTHORIZED', message: '请先登录' }); } // 1. 获取用户所属角色的全部权限集合 const userRole = user.role || 'free_user'; const userPermissions = ROLE_PERMISSIONS[userRole] || []; // 2. 判定是否拥有目标权限 const hasPermission = userPermissions.includes(requiredPermission); if (!hasPermission) { return reply.status(403).send({ code: 'FORBIDDEN', message: '您的账户权限不足,请升级套餐或联系管理员', required: requiredPermission }); } }; }核心实现三:在路由中声明式应用与资源归属二次校验
在 API 路由定义中,鉴权逻辑变得前所未有的优雅和清晰:
// src/routes/reportRoutes.ts import { FastifyInstance } from 'fastify'; import { requirePermission } from '../middlewares/requirePermission'; import { PERMISSIONS } from '../auth/permissions'; export async function reportRoutes(app: FastifyInstance) { // 仅具备 REPORT_CREATE 权限的用户可访问 app.post( '/api/reports', { preHandler: [requirePermission(PERMISSIONS.REPORT_CREATE)] }, async (req, reply) => { // 业务逻辑... return reply.send({ success: true }); } ); // 删除周报接口:结合资源归属属性校验(ABAC) app.delete( '/api/reports/:id', { preHandler: [requirePermission(PERMISSIONS.REPORT_DELETE)] }, async (req, reply) => { const { id } = req.params as any; const user = (req as any).user; const report = await db.queryReport(id); if (!report) return reply.status(404).send('周报不存在'); // 如果非管理员,只能删除属于自己的周报 if (user.role !== 'team_admin' && report.userId !== user.id) { return reply.status(403).send({ error: '您只能删除自己创建的周报记录' }); } await db.deleteReport(id); return reply.send({ success: true }); } ); }架构收益总结
- 权限与业务代码彻底解耦:新增权限点或调整角色策略时,只需修改统一的权限映射表,无需改动任何 Controller 业务逻辑;
- 前后端权限清单复用:前端可以通过用户登录后下发的
permissions: string[]数组,结合自定义指令实现按钮级(Button-Level)的精准显隐控制; - 安全审计追踪:每一次拦截均可记录到安全审计日志中,有效抵御越权漏洞(IDOR)。