使用Nest.js设计RBAC权限系统:分步指南

前言

对于后台管理系统而言,「访问控制」和「个性化界面」是必备功能。

举例:超级管理员能查看所有页面;普通用户只能访问 A 和 B 页面;VIP 用户则能查看 A、B、C、D 四个页面。

这些功能背后依赖的是三大核心概念:

  • 用户(User) :最小单位,如 Alice、Bob、Charlie。
  • 角色(Role) :一个用户可以拥有多个角色。例如 Alice 既是普通用户又是 VIP。
  • 权限(Permission) :角色会关联若干权限。VIP 角色可能拥有「查看、编辑、新增」权限,超级管理员则拥有「查看、编辑、新增、删除」权限。

三者关系可用下图概括。

接下来,我们将用 Nest.js 从零开始搭建一套这样的权限体系。

创建数据库

首先建库。我们使用 MySQL,执行:

js 复制代码
CREATE DATABASE `nest-database` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

初始化项目

新建 Nest 项目:

js 复制代码
nest new nest-project

安装数据库依赖,主要是 typeormmysql2

js 复制代码
npm install --save @nestjs/typeorm typeorm mysql2

app.module.ts 中配置 TypeORM:

js 复制代码
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'nest-database',
      synchronize: true,
      logging: true,
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      poolSize: 10,
      connectorPackage: 'mysql2',
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

表结构设计

典型的 RBAC 系统需要 5 张表:

  • user:用户表,存储用户名、密码、邮箱等基本信息。
  • role:角色表,存储角色名称、角色编码等。
  • permission:权限表,存储权限名称、权限编码等。
  • user_role_relation:用户与角色的多对多关系表。
  • role_permission_relation:角色与权限的多对多关系表。

整体关系如下图。

接下来在 Nest 中创建三张实体表并定义关联。

user.entity.ts

js 复制代码
import {
  Column,
  CreateDateColumn,
  Entity,
  JoinTable,
  ManyToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';
import { Role } from './role.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 50 })
  username: string;

  @Column({ length: 50 })
  password: string;

  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;

  @ManyToMany(() => Role)
  @JoinTable({
    name: 'user_role_relation',
    joinColumn: { name: 'userId', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'roleId', referencedColumnName: 'id' },
  })
  roles: Role[];
}

User 表通过 roles 字段与中间表 user_role_relation 关联:
user.id === userRoleRelation.userIdrole.id === userRoleRelation.roleId

匹配的 Role 记录会自动挂载到 User

role.entity.ts

js 复制代码
import {
  Column,
  CreateDateColumn,
  Entity,
  JoinTable,
  ManyToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';
import { Permission } from './permission.entity';

@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 20 })
  name: string;

  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;

  @ManyToMany(() => Permission)
  @JoinTable({
    name: 'role_permission_relation',
    joinColumn: { name: 'roleId', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'permissionId', referencedColumnName: 'id' },
  })
  permissions: Permission[];
}

Role 表通过 permissions 字段与中间表 role_permission_relation 关联,逻辑同上。

permission.entity.ts

js 复制代码
import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity()
export class Permission {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 50 })
  name: string;

  @Column({ length: 100, nullable: true })
  desc: string;

  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;
}

Permission 表仅记录可用权限,不与其他表做直接关联。

数据初始化

下面是一个初始化测试数据的服务:

js 复制代码
async function initData() {
  const user1 = new User();
  user1.username = 'Alice';
  user1.password = 'aaaaaa';

  const user2 = new User();
  user2.username = 'Bob';
  user2.password = 'bbbbbb';

  const user3 = new User();
  user3.username = 'Charlie';
  user3.password = 'cccccc';

  const role1 = new Role();
  role1.name = 'Administrator';

  const role2 = new Role();
  role2.name = 'Regular User';

  const permission1 = new Permission();
  permission1.name = 'Add resource_a';

  const permission2 = new Permission();
  permission2.name = 'Edit resource_a';

  const permission3 = new Permission();
  permission3.name = 'Delete resource_a';

  const permission4 = new Permission();
  permission4.name = 'Query resource_a';

  const permission5 = new Permission();
  permission5.name = 'Add resource_b';

  const permission6 = new Permission();
  permission6.name = 'Edit resource_b';

  const permission7 = new Permission();
  permission7.name = 'Delete resource_b';

  const permission8 = new Permission();
  permission8.name = 'Query resource_b';

  role1.permissions = [
    permission1,
    permission2,
    permission3,
    permission4,
    permission5,
    permission6,
    permission7,
    permission8,
  ];

  role2.permissions = [permission1, permission2, permission3, permission4];

  user1.roles = [role1];
  user2.roles = [role2];

  await this.entityManager.save(Permission, [
    permission1,
    permission2,
    permission3,
    permission4,
    permission5,
    permission6,
    permission7,
    permission8,
  ]);

  await this.entityManager.save(Role, [role1, role2]);
  await this.entityManager.save(User, [user1, user2]);
}

通过浏览器或 Postman 调用该 initData 服务,即可完成数据写入。

原文:dev.to/leapcell/de...

相关推荐
Eric_见嘉2 天前
NestJS 🧑‍🍳 厨子必修课(九):API 文档 Swagger
前端·后端·nestjs
XiaoYu200210 天前
第3章 Nest.js拦截器
前端·ai编程·nestjs
XiaoYu200212 天前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄12 天前
NestJS 调试方案
后端·nestjs
当时只道寻常16 天前
NestJS 如何配置环境变量
nestjs
濮水大叔1 个月前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
ovensi1 个月前
告别笨重的 ELK,拥抱轻量级 PLG:NestJS 日志监控实战指南
nestjs
ovensi1 个月前
Docker+NestJS+ELK:从零搭建全链路日志监控系统
后端·nestjs
Gogo8161 个月前
nestjs 的项目启动
nestjs