Nest.js 系列——在 Nest.js 中使用 rbac 进行权限控制

前言

一般后台的系统都是需要权限控制的,而且最常用的就是 rbac 权限控制,这里我们就来看看如何在 nest 中使用 rbac 进行权限控制。rbac 的权限控制主要分为三个部分,用户、角色、权限,用户和角色是多对多的关系,角色和权限也是多对多的关系,用户和权限是多对多的关系,这里我们就来看看如何在 nest 中使用 rbac 进行权限控制。

由上图可以看出,先把权限分配给角色,然后在把角色分配给用户,这样就可以实现权限控制了。如果想要修改一个用户的权限,只需要修改角色的权限就可以了,这样就可以实现权限的统一管理。那就在 nest 简单实践先 rabc 权限吧。

创建数据库

首先我们需要创建数据库,这里我们使用 mysql 数据库,创建数据库的 sql 语句如下:

sql 复制代码
CREATE DATABASE `nest_rbac` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

创建项目

创建项目的命令如下:

bash 复制代码
nest new nest-rbac

安装 typorm 相关依赖

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

配置 typeorm 相关配置,打开app.module.ts文件:

typescript 复制代码
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: '123456',
      database: 'nest_rbac',
      synchronize: true,
      logging: true,
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      poolSize: 10,
      connectorPackage: 'mysql2'
    })
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

创建用户模块

bash 复制代码
nest g resource user

数据库表之间的关系如图所示:

然后添加用户、角色、权限的实体类,在user/entity文件夹下添加:

  • user.entity.ts
typescript 复制代码
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'
  })
  roles: Role[]
}

通过@ManyToMany 装饰器来表示多对多的关系,然后通过@JoinTable 来指定关联表user_role_relation

  • role.entity.ts
typescript 复制代码
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'
  })
  permissions: Permission[]
}

通过@ManyToMany 装饰器来表示多对多的关系,然后通过@JoinTable 来指定关联表role_permission_relation

  • permission.entity.ts
typescript 复制代码
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
}

然后重新运行项目,会自动创建表。这样一个初始化项目就建立好了,接下来开始实现 rbac 权限控制。首先先对数据库添加一些测试数据,通过定义一个接口方法来添加。在user/user.service.ts文件中添加一个 initData 方法来初始化数据:

typescript 复制代码
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { InjectEntityManager } from '@nestjs/typeorm'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { EntityManager, In } from 'typeorm'
import { User } from './entities/user.entity'
import { Role } from './entities/role.entity'
import { Permission } from './entities/permission.entity'
import { LoginUserDto } from './dto/login-user.dto'

@Injectable()
export class UserService {
  @InjectEntityManager()
  entityManager: EntityManager

  async initData() {
    const user1 = new User()
    user1.username = '张三'
    user1.password = '111111'

    const user2 = new User()
    user2.username = '李四'
    user2.password = '222222'

    const user3 = new User()
    user3.username = '王五'
    user3.password = '333333'

    const role1 = new Role()
    role1.name = '管理员'

    const role2 = new Role()
    role2.name = '普通用户'

    const permission1 = new Permission()
    permission1.name = '新增 aaa'

    const permission2 = new Permission()
    permission2.name = '修改 aaa'

    const permission3 = new Permission()
    permission3.name = '删除 aaa'

    const permission4 = new Permission()
    permission4.name = '查询 aaa'

    const permission5 = new Permission()
    permission5.name = '新增 bbb'

    const permission6 = new Permission()
    permission6.name = '修改 bbb'

    const permission7 = new Permission()
    permission7.name = '删除 bbb'

    const permission8 = new Permission()
    permission8.name = '查询 bbb'

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

  async login(loginUserDto: LoginUserDto) {
    const user = await this.entityManager.findOne(User, {
      where: {
        username: loginUserDto.username
      },
      relations: ['roles']
    })

    if (!user) {
      throw new HttpException('用户不存在', HttpStatus.ACCEPTED)
    }

    if (user.password !== loginUserDto.password) {
      throw new HttpException('密码错误', HttpStatus.ACCEPTED)
    }

    return user
  }

  async findRolesByIds(roleIds) {
    return this.entityManager.find(Role, {
      where: {
        id: In(roleIds)
      },
      relations: {
        permissions: true
      }
    })
  }

  findOne(id: number) {
    return `This action returns a #${id} user`
  }

  update(id: number, updateUserDto: UpdateUserDto) {
    return `This action updates a #${id} user`
  }

  remove(id: number) {
    return `This action removes a #${id} user`
  }
}

以上初始化的 rbac 权限如图所示:

定义一个 user 的控制器路由来初始化数据,在user/user.controller.ts文件中添加:

typescript 复制代码
@Get('init')
async initData() {
    await this.userService.initData();
    return 'done';
}

直接调用初始化数据接口,然后就可以在数据库中看到初始化的数据了。

使用 jwt 实现登录

关于登录就不展开说了,如果还不明白的可以看下之前的 jwt 文章,这里直接使用 jwt 实现登录。首先定义下登录的路由,在user/user.controller.ts文件中添加:

typescript 复制代码
@Post('login')
async login(@Body() loginUserDto: LoginUserDto) {
    const user = await this.userService.login(loginUserDto);
    const payload = {
        username: user.username,
        sub: user.id,
        roles: user.roles.map(role => role.name),
        permissions: user.roles.map(role => role.permissions.map(permission => permission.name)).flat()
    };
    return {
        access_token: this.jwtService.sign(payload)
    };
}

dto 文件夹下需要对参数进行校验,需要安装class-validatorclass-transformer依赖:

bash 复制代码
npm install --save class-validator class-transformer

然后在user/dto文件夹下添加login-user.dto.ts文件:

typescript 复制代码
import { IsNotEmpty } from 'class-validator'

export class LoginUserDto {
  @IsNotEmpty()
  @Length(1, 50)
  username: string

  @IsNotEmpty()
  @Length(1, 50)
  password: string
}

然后在user/user.service.ts文件中添加 login 方法:

typescript 复制代码
async login(loginUserDto: LoginUserDto) {
    const user = await this.entityManager.findOne(User, {
        where: {
            username: loginUserDto.username
        },
        relations: ['roles']
    });

    if (!user) {
        throw new HttpException('用户不存在', HttpStatus.ACCEPTED);
    }

    if (user.password !== loginUserDto.password) {
        throw new HttpException('密码错误', HttpStatus.ACCEPTED);
    }

    return user;
}

需要全局开启验证,打开main.ts文件:

typescript 复制代码
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  // 开启全局验证
  app.useGlobalPipes(new ValidationPipe())
  await app.listen(3000)
}
bootstrap()

需要把 user 的信息放到 jwt,安装@nestjs/jwt依赖:

bash 复制代码
npm install --save @nestjs/jwt

然后在app.module.ts文件中添加:

typescript 复制代码
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { TypeOrmModule } from '@nestjs/typeorm'
import { UserModule } from './user/user.module'
import { JwtModule } from '@nestjs/jwt'

@Module({
  imports: [
      JwtModule.register({
        global: true,
        secret: 'water',
        signOptions: {
          expiresIn: '1d'
        }
      })
  ]
})

设置为全局模块,然后各个模块都可以使用了。

tsx 复制代码
@Post('login')
async login(@Body() loginUser: UserLoginDto){
  const user = await this.userService.login(loginUser);

  const token = this.jwtService.sign({
    user: {
      username: user.username,
      roles: user.roles
    }
  });

  return {
      token
  }
}

这样就通过 jwt 实现了登录,然后就可以通过 token 来获取用户信息了。但是现在有一个问题就说所有的接口都需要登录鉴权才能访问了,其实有些接口是不需要登录鉴权的,比如登录接口,这里我们就需要对接口进行分类,然后对不同的接口进行不同的鉴权。这里我们就需要使用到 nest 的路由守卫了。

使用路由守卫实现鉴权

使用 cli 创建一个路由守卫:

bash 复制代码
nest g guard login --no-spec --flat

login.guard.ts文件中修改判断逻辑:

typescript 复制代码
import {
  CanActivate,
  ExecutionContext,
  Inject,
  Injectable,
  UnauthorizedException
} from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Request } from 'express'
import { Observable } from 'rxjs'

declare module 'express' {
  interface Request {
    user: {
      username: string
      roles: Role[]
    }
  }
}

@Injectable()
export class LoginGuard implements CanActivate {
  @Inject(JwtService)
  private jwtService: JwtService

  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const request: Request = context.switchToHttp().getRequest()

    const authorization = request.headers.authorization

    if (!authorization) {
      throw new UnauthorizedException('用户未登录')
    }

    try {
      const token = authorization.split(' ')[1]
      const data = this.jwtService.verify(token)
      request.user = data.user
      return true
    } catch (e) {
      throw new UnauthorizedException('token 失效,请重新登录')
    }
  }
}

如果在每一个需要鉴权的路由上加守卫,有点太麻烦了,这里可以直接在全局加上守卫。在 app.module.ts 文件中添加:

typescript 复制代码
providers: [
  {
    provide: APP_GUARD,
    useClass: LoginGuard
  }
]

这样这个守卫就会对所有的路由进行鉴权了,但是这样就会有一个问题,就是登录接口也会被鉴权,这样就会导致登录接口无法访问了,这里我们就需要对登录接口进行排除。先声明一个自定义装饰器,来标识哪些接口需要鉴权。

typescript 复制代码
import { SetMetadata } from '@nestjs/common'

export const RequireLogin = () => SetMetadata('require-login', true)

然后需要改造下 LoginGuard,来判断是否需要鉴权。

typescript 复制代码
const requireLogin = this.reflector.getAllAndOverride('require-login', [
  context.getClass(),
  context.getHandler()
])

console.log(requireLogin)

if (!requireLogin) {
  return true
}

这样登录鉴权的功能就实现了,但是这里还有一个问题,就是如果用户没有权限访问某个接口,这里就需要对接口进行权限控制了。创建一个权限守卫:

bash 复制代码
nest g guard permission --no-spec --flat

permission.guard.ts文件中添加:

typescript 复制代码
import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common'
import { Request } from 'express'
import { Observable } from 'rxjs'
import { UserService } from './user.service'

@Injectable()
export class PermissionGuard implements CanActivate {
  @Inject(UserService)
  private userService: UserService

  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    console.log(this.userService)

    return true
  }
}

userservice 中添加一个方法,来查询角色信息:

typescript 复制代码
async findRolesByIds(roleIds: number[]) {
    return this.entityManager.find(Role, {
      where: {
        id: In(roleIds)
      },
      relations: {
        permissions: true
      }
    });
}

然后在permission.guard.ts文件中添加:

typescript 复制代码
import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common'
import { Request } from 'express'
import { UserService } from './user/user.service'

@Injectable()
export class PermissionGuard implements CanActivate {
  @Inject(UserService)
  private userService: UserService

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request: Request = context.switchToHttp().getRequest()

    if (!request.user) {
      return true
    }

    const roles = await this.userService.findRolesByIds(request.user.roles.map((item) => item.id))

    const permissions: Permission[] = roles.reduce((total, current) => {
      total.push(...current.permissions)
      return total
    }, [])

    console.log(permissions)

    return true
  }
}

这样就可以获取到用户的权限了,然后就可以对接口进行权限控制了。这里我们就需要在自定义装饰器中添加一个权限的标识。

typescript 复制代码
export const RequirePermission = (permissions: string[]) =>
  SetMetadata('require-permission', permissions)

然后在permission.guard.ts文件中添加:

typescript 复制代码
const requirePermission = this.reflector.getAllAndOverride<string[]>('require-permission', [
  context.getClass(),
  context.getHandler()
])

for (let i = 0; i < requiredPermissions.length; i++) {
  const curPermission = requiredPermissions[i]
  const found = permissions.find((item) => item.name === curPermission)
  if (!found) {
    throw new UnauthorizedException('您没有访问该接口的权限')
  }
}

小结

通过以上的实践,我们就可以在 nest 中使用 rbac 进行权限控制了,这里只是简单实践了下。在业务中使用还需要多多练习。

相关推荐
yanglamei19621 分钟前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝2 分钟前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
Adolf_19935 分钟前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
dot.Net安全矩阵13 分钟前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
叫我:松哥17 分钟前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼19 分钟前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
Hellc00724 分钟前
MacOS升级ruby版本
前端·macos·ruby
工业甲酰苯胺30 分钟前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
前端西瓜哥33 分钟前
贝塞尔曲线算法:求贝塞尔曲线和直线的交点
前端·算法
又写了一天BUG34 分钟前
npm install安装缓慢及npm更换源
前端·npm·node.js