logoLion
首页分类标签
© 2026 Lion
Just for fun!
Built with NodeBBS v1.8.0

📅 RBAC 重构计划 (多角色版)

开放中•node发布于 1 个月前

第一阶段:数据库架构升级 (Database Schema)

我们将引入标准的 RBAC 数据库模型。

  1. 创建 roles 表 (角色定义)
    • 存储角色的元数据。
    • 字段:id, name (唯一标识, 如 'admin'), displayName (显示名), description, isSystem (防止删除核心角色), color (用于前端徽章显示)。
  2. 创建 permissions 表 (权限定义)
    • 存储系统中所有可执行的操作。
    • 字段:id, slug (唯一标识, 如 'topic:create'), description, category (分组, 如 'forum', 'moderation').
  3. 创建 role_permissions 表 (角色-权限关联)
    • 定义每个角色拥有哪些权限。
    • 字段:roleId, permissionId。
  4. 创建 users_roles 表 (用户-角色关联) ★ 核心变更
    • 实现多角色支持。
    • 字段:userId, roleId。
    • 注:现有的 users.role 字段将保留用于兼容旧逻辑(作为“主角色”或“显示角色”),但权限检查将依赖这张新表。

第二阶段:数据迁移与初始化 (Migration & Seeding)

这是最关键的一步,确保现有数据平滑过渡。

  1. 编写 Seed 脚本 (apps/api/src/scripts/seed-rbac.js)
    • Step 1: 初始化权限池
      • 扫描代码逻辑,注册所有权限(例如:file:upload, topic:pin, user:ban, invitation:create 等)。
    • Step 2: 初始化基础角色
      • 创建 admin (拥有所有权限)。
      • 创建 moderator (拥有管理内容权限)。
      • 创建 vip (拥有高级功能权限)。
      • 创建 user (基础权限)。
    • Step 3: 迁移现有用户数据
      • 读取 users 表中所有用户。
      • 根据用户的 role 字段(字符串),在 users_roles 表中创建对应的记录。
      • 结果:原本 role='admin' 的用户,现在会在 users_roles 中有一条关联到 Admin 角色的记录。

第三阶段:后端核心改造 (Backend Core)

  1. 升级认证插件 (apps/api/src/plugins/auth.js)
    • 在用户登录/鉴权时,通过 users_roles -> roles -> role_permissions -> permissions 链条查询。
    • 将所有权限 Slug 扁平化为一个数组 user.permissions = ['p1', 'p2', ...]。
    • 在 request.user 上挂载辅助函数:user.hasRole('admin') 和 user.can('topic:create')。
  2. 开发角色管理 API (apps/api/src/routes/admin/roles)
    • GET /admin/roles: 获取角色列表(含权限)。
    • POST /admin/roles: 创建新角色。
    • PUT /admin/roles/:id: 修改角色(名称、颜色)。
    • PUT /admin/roles/:id/permissions: 分配权限。
    • GET /admin/permissions: 获取所有可用权限列表(用于前端勾选)。

第四阶段:业务逻辑替换 (Refactoring)

  1. 替换硬编码检查
    • 查找代码中的 if (user.role === 'admin')。
    • 替换为 if (user.can('system:admin')) 或 if (user.hasRole('admin'))。
    • 重点修改:posts/index.js (发帖权限), upload/index.js (上传权限), admin/ (后台权限)。
  2. 重构邀请规则
    • 修改 invitation_rules 表,将 role 字段(字符串)改为关联 role_id(外键)。
    • 这意味着管理员可以为自己创建的 "超级版主" 角色单独设定邀请码生成规则。

第五阶段:前端对接 (Frontend)

  1. 管理后台 (apps/web/src/app/dashboard/roles)
    • 新增角色管理模块:增删改查角色。
    • 权限矩阵界面:一个表格,行是权限,列是角色,通过复选框快速分配权限。
  2. 用户管理
    • 在编辑用户界面,允许通过多选框(Multi-select)修改用户的角色(不再是单选下拉框)。

=====================================

// ============ RBAC (Role-Based Access Control) ============

// ============ Roles (角色) ============

// ============ RBAC: Roles (角色) ============ export const roles = pgTable( 'roles', { ...$defaults, name: varchar('name', { length: 50 }).notNull().unique(), displayName: varchar('display_name', { length: 100 }).notNull(), description: text('description'), isSystem: boolean('is_system').notNull().default(false), isDefault: boolean('is_default').notNull().default(false), color: varchar('color', { length: 7 }).default('#000000'), }, (table) => [ index('roles_name_idx').on(table.name), index('roles_is_system_idx').on(table.isSystem), index('roles_is_default_idx').on(table.isDefault), ] ); export const rolesRelations = relations(roles, ({ many }) => ({ users: many(usersRoles), permissions: many(rolePermissions), invitationRules: many(invitationRules, { relationName: 'role' }), })); // ============ RBAC: Permissions (权限) ============ export const permissions = pgTable( 'permissions', { ...$defaults, slug: varchar('slug', { length: 100 }).notNull().unique(), description: text('description'), category: varchar('category', { length: 50 }), }, (table) => [ index('permissions_slug_idx').on(table.slug), index('permissions_category_idx').on(table.category), ] ); export const permissionsRelations = relations(permissions, ({ many }) => ({ roles: many(rolePermissions), })); // ============ RBAC: Role Permissions (角色权限关联) ============ export const rolePermissions = pgTable( 'role_permissions', { roleId: integer('role_id') .notNull() .references(() => roles.id, { onDelete: 'cascade' }), permissionId: integer('permission_id') .notNull() .references(() => permissions.id, { onDelete: 'cascade' }), createdAt: $createdAt, }, (table) => [ unique('role_permissions_role_permission_unique').on(table.roleId, table.permissionId), index('role_permissions_role_idx').on(table.roleId), index('role_permissions_permission_idx').on(table.permissionId), ] ); export const rolePermissionsRelations = relations(rolePermissions, ({ one }) => ({ role: one(roles, { fields: [rolePermissions.roleId], references: [roles.id], }), permission: one(permissions, { fields: [rolePermissions.permissionId], references: [permissions.id], }), })); // ============ RBAC: User Roles (用户角色关联) ============ export const usersRoles = pgTable( 'users_roles', { userId: integer('user_id') .notNull() .references(() => users.id, { onDelete: 'cascade' }), roleId: integer('role_id') .notNull() .references(() => roles.id, { onDelete: 'cascade' }), createdAt: $createdAt, }, (table) => [ unique('users_roles_user_role_unique').on(table.userId, table.roleId), index('users_roles_user_idx').on(table.userId), index('users_roles_role_idx').on(table.roleId), ] ); export const usersRolesRelations = relations(usersRoles, ({ one }) => ({ user: one(users, { fields: [usersRoles.userId], references: [users.id], }), role: one(roles, { fields: [usersRoles.roleId], references: [roles.id], }), }));
// ============ Invitation Rules (邀请规则) ============ export const invitationRules = pgTable( 'invitation_rules', { ...$defaults, role: varchar('role', { length: 20 }).default('user'), // Legacy field for backward compatibility roleId: integer('role_id').references(() => roles.id, { onDelete: 'set null', }), dailyLimit: integer('daily_limit').notNull().default(1), maxUsesPerCode: integer('max_uses_per_code').notNull().default(1), expireDays: integer('expire_days').notNull().default(30), pointsCost: integer('points_cost').notNull().default(0), isActive: boolean('is_active').notNull().default(true), }, (table) => [ index('invitation_rules_role_idx').on(table.role), index('invitation_rules_role_id_idx').on(table.roleId), index('invitation_rules_is_active_idx').on(table.isActive), ] ); export const invitationRulesRelations = relations(invitationRules, ({ one }) => ({ role: one(roles, { fields: [invitationRules.roleId], references: [roles.id], relationName: 'role', }), }));

=====================

RBAC 架构重构最终方案 (RBAC Refactoring Master Plan)

  1. 数据库架构升级 (apps/api/src/db/schema.js) 采用多角色(Many-to-Many)模型,并保留兼容性。
  • 新增表:
    • roles: 角色定义(id, name, displayName, description, isSystem, isDefault)。
    • permissions: 权限池(id, slug, description, category)。
    • rolePermissions: 角色-权限关联表。
    • usersRoles: 用户-角色关联表(userId, roleId)。
  • 修改表:
    • invitationRules: 将 role 字段(字符串)改为 roleId(外键),或保留 role 字符串但改为存储 roles.name 以保持逻辑一致性(建议使用外键 roleId 更稳健)。
    • users: 保留 role 字段用于向后兼容(逻辑上作为“主显示角色”),但在鉴权时优先使用 usersRoles 表的数据。
  1. 初始化脚本与数据迁移 (apps/api/src/scripts/init/) 参考 apps/api/src/scripts/init/index.js 的模块化结构。
  • 新建 apps/api/src/scripts/init/rbac.js:
    • 定义 DEFAULT_PERMISSIONS: 按资源分类(如 topic:read, topic:create, topic:edit, topic:delete, topic:pin, ui:admin_dashboard 等)。
    • 定义 DEFAULT_ROLES: Admin, Moderator, User, VIP。
    • 实现 initRBAC(db):
      1. 同步权限池(确保代码中定义的新权限写入数据库)。
      2. 创建/更新默认角色。
      3. 为默认角色分配默认权限。
      4. (关键) 迁移旧用户数据:扫描 users 表,根据 role 字段在 usersRoles 表中创建对应记录。
  • 更新 apps/api/src/scripts/init/index.js:
    • 引入并调用 initRBAC,确保执行 npm run seed 时自动应用。
  1. 后端核心鉴权 (apps/api/src/plugins/auth.js)
  • User Object 增强:
    • 登录/获取用户信息时,执行 SQL Join 查询该用户的所有角色及对应权限。
    • 将权限扁平化并注入到 request.user 对象中,例如: request.user.permissions = ['topic:read', 'topic:create', 'file:upload']; request.user.roles = ['admin', 'vip']; // 角色名列表 * 输出到前端: 确保 /api/auth/me 或用户信息接口返回 permissions 数组,以便前端使用。
  1. 后台管理 API (apps/api/src/routes/admin/) 遵循 RESTful 规范,并参考 captcha/index.js 使用 preHandler 进行权限守卫。
  • 路径: apps/api/src/routes/admin/roles/index.js
  • 守卫: // 示例 preHandler const requireAdminPermission = async (req, reply) => { if (!req.user.permissions.includes('sys:manage_roles')) { throw reply.forbidden('需要角色管理权限'); } };
    • 接口:
    • GET /: 获取角色列表(含权限统计)。
    • POST /: 创建新角色。
    • GET /:id: 获取单个角色详情(含已分配权限)。
    • PUT /:id: 更新角色信息。
    • DELETE /:id: 删除角色(系统角色除外)。
    • POST /:id/permissions: 更新该角色的权限集合。
  • 路径: apps/api/src/routes/admin/permissions/index.js
    • GET /: 获取所有系统可用权限(用于前端构建“权限矩阵”复选框)。
  1. 前端权限控制 (apps/web)
  • 获取权限:
    • 用户登录后,API 返回的用户信息应包含 permissions: []。
    • 前端 Store (如 Zustand 或 Context) 存储此权限列表。
  • UI 组件控制:
    • 封装 <PermissionGuard slug="topic:pin"> 组件或使用 hook usePermission('topic:pin')。
  • 改造 TopicSidebar.jsx:
    • 旧逻辑: if (user.role === 'admin' || user.role === 'moderator')
    • 新逻辑: if (user.permissions.includes('topic:pin'))
    • 这样,如果你新建了一个“实习版主”角色并赋予了置顶权限,该按钮会自动对该角色可见。

请先登录后再发表评论

N

node

分类

NodeBBS
浏览数41
最后编辑1 个月前
编辑次数3