NestJs+TypeORM+MySQL实现RBAC的简易案例

625 阅读2分钟

仓库地址:github.com/Oorpow/nest…

1. TypeORM 连接 MySQL

安装依赖:

npm install --save typeorm mysql2 @nestjs/typeorm

1.1 配置环境变量

npm i @nestjs/config --save

创建.env文件

MYSQL_HOST='localhost'
MYSQL_PORT=3306
MYSQL_USERNAME='root'
MYSQL_PASSWORD='xxxx'
# 要连接的数据库名称
MYSQL_DATABASE='test'

全局加载环境变量

import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { ConfigModule } from '@nestjs/config'

import { AppController } from './app.controller'
import { AppService } from './app.service'
import config from './common/config'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [config],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

1.2 TypeORM注册

app.module.ts

import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { ConfigModule } from '@nestjs/config'

import { AppController } from './app.controller'
import { AppService } from './app.service'
import config from './common/config'

@Module({
  imports: [
    // 配置全局的环境变量
    ConfigModule.forRoot({
      isGlobal: true,
      load: [config],
    }),
    // 连接MySQL
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: process.env.MYSQL_HOST,
      port: process.env.MYSQL_PORT as unknown as number,
      username: process.env.MYSQL_USERNAME,
      password: process.env.MYSQL_PASSWORD,
      database: process.env.MYSQL_DATABASE,
      synchronize: true,
      // 自动加载实体
      autoLoadEntities: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

1.3 创建实体

关联关系:

  • 用户 <=> 角色 (1对多)
  • 角色 <=> 权限 (多对多)

User实体:

import { Role } from 'src/role/entities/role.entity'

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number
  @Column()
  username: string
  @Column()
  password: string

  //   一个用户对应一个角色,一个角色对应多个用户
  @ManyToOne(() => Role, (role) => role.user)
  @JoinColumn({ name: 'roleId' })
  role: Role
}

Role(角色)实体:

import { Permission } from 'src/permission/entities/permission.entity'
import { User } from 'src/user/entities/user.entity'

@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number
  @Column()
  name: string

  @OneToMany(() => User, (user) => user.role)
  @JoinColumn({ name: 'userId' })
  user: User

  @ManyToMany(() => Permission)
  @JoinTable()
  permissions: Permission[]
}

Permission实体:

@Entity()
export class Permission {
  @PrimaryGeneratedColumn()
  id: number
  @Column()
  name: string
}

在role.module.ts中进行关联:

@Module({
  imports: [TypeOrmModule.forFeature([User, Role, Permission])],
  controllers: [RoleController],
  providers: [RoleService],
})
export class RoleModule {}

在user.module.ts中进行关联:

@Module({
  imports: [TypeOrmModule.forFeature([Role, User])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

1.4 Database Client

查看TypeORM生成的表,这里通过VsCode的插件Database Client

image.png

2. 角色和权限设计

角色:

  • 用户
  • 管理员

权限(模块:操作):

  • 用户:Query
  • 管理员:CRUD

数据表示例:

image.png

image.png

3. 具体实现

创建auth模块用于权限验证

// auth.controller.ts
import { AuthService } from './auth.service'

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('permission')
  async hasPermission(@Body() body) {
    const { userId, permissionName } = body
    return this.authService.hasPermission(userId, permissionName)
  }
}

具体验证逻辑:

import { RoleService } from 'src/role/role.service'
import { UserService } from 'src/user/user.service'

@Injectable()
export class AuthService {
  // 注意:这里用到的service需要在原来的module.ts的exports进行导出,并在auth.module.ts中进行引入
  constructor(
    private readonly userService: UserService,
    private readonly roleService: RoleService,
  ) {}

  async hasPermission(userId: number, permissionName: string) {
    // 根据用户id查询用户信息
    const user = await this.userService.findOne(userId)
    const { password, ...result } = user
    // 根据用户的角色id查询该角色对应的权限列表
    const { permissions } = await this.roleService.findOne(result.role.id)
    
    // 权限校验
    if (permissions && permissions.find((p) => p.name === permissionName)) {
      return true
    }
    return false
  }
}

一对多关联查询:

// user.service.ts
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private readonly userRepo: Repository<User>,
  ) {}
  
  async findOne(id: number) {
    return this.userRepo.findOneOrFail({
      where: {
        id,
      },
      relations: ['role'],
    })
  }
}
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { Role } from './entities/role.entity'

@Injectable()
export class RoleService {
  constructor(
    @InjectRepository(Role) private readonly roleRepo: Repository<Role>,
  ) {}

  async findOne(id: number) {
    return this.roleRepo.findOneOrFail({
      where: {
        id,
      },
      relations: ['permissions'],
    })
  }
}

4. ApiFox 测试

为了测试,直接在user表添加两条数据,并分别给定角色为管理员(1)和用户(2); 并且,前面提到过用户只允许查询、管理员拥有所有权限,因此还需要在连接表中分配对应的权限;

用户表:

image.png

连接表:分配权限

image.png

管理员测试:

image.png

用户测试:

image.png