Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现

RBAC简介

RBAC(Role Based Access Control)是基于角色的权限控制,简单来说就是给用户赋予一些角色,那么该用户就会拥有这些角色的所有权限。接下来我们就用 NestJS 来实现基于 RBAC 的权限控制

创建表

除了我们已有的 user 表,我们还需创建角色表(role),权限字段表(permission)。可以直接用命令新增两个模块

nest g res role
nest g res permission

进入权限 permission 模块的 entity 中编写 permission 表的字段

//permission.entity.ts
import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
from 'typeorm';

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

  @Column({
    length50,
  })
  name: string;

  @Column({
    length100,
    nullabletrue,
  })
  desc: string;

  @CreateDateColumn()
  createTimeDate;

  @UpdateDateColumn()
  updateTimeDate;
}

一个权限可以赋给多个角色,同时一个角色也可以有多个权限,因此它们是多对多(ManyToMany)的关系,所以我们可以用role_permission_relation关系表将它们关联起来,角色(role)模块下的 entity 如下

//role.entity.ts
import {
  Column,
  CreateDateColumn,
  Entity,
  JoinTable,
  ManyToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
from 'typeorm';
import { Permission } from '../../permission/entities/permission.entity';
@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: string;

  @Column({
    length20,
  })
  name: string;

  @CreateDateColumn()
  createTimeDate;

  @UpdateDateColumn()
  updateTimeDate;

  @ManyToMany(() => Permission)
  @JoinTable({
    name'role_permission_relation',
  })
  permissions: Permission[];
}

同样的用户(user)与角色(role)直接也是多对多的关系,因此 user 的 entity 修改如下

import {
  Column,
  Entity,
  PrimaryGeneratedColumn,
  BeforeInsert,
  ManyToMany,
  JoinTable,
from 'typeorm';
import encry from '../../utils/crypto';
import * as crypto from 'crypto';
import { Role } from '../../role/entities/role.entity';
@Entity('user')
export class User {
  @PrimaryGeneratedColumn()
  id: string; // 标记为主键,值自动生成

  @Column({ length30 })
  username: string; //用户名
  @Column({ nullabletrue })
  nickname: string; //昵称
  @Column()
  password: string; //密码
  @Column({ nullabletrue })
  avatar: string; //头像
  @Column({ nullabletrue })
  email: string; //邮箱

  @ManyToMany(() => Role)
  @JoinTable({
    name'user_role_relation',
  })
  roles: Role[]; //角色

  @Column({ nullabletrue })
  salt: string;
  @Column({ type'timestamp'default() => 'CURRENT_TIMESTAMP' })
  create_timeDate;

  @Column({ type'timestamp'default() => 'CURRENT_TIMESTAMP' })
  update_timeDate;
  @BeforeInsert()
  beforeInsert() {
    this.salt = crypto.randomBytes(4).toString('base64');
    this.password = encry(this.password, this.salt);
  }
}

最后在各自的 module 导入实体,以 permission.module.ts 为例

import { Module } from '@nestjs/common';
import { PermissionService } from './permission.service';
import { PermissionController } from './permission.controller';
import { Permission } from './entities/permission.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
  controllers: [PermissionController],
  providers: [PermissionService],
  imports: [TypeOrmModule.forFeature([Permission])],
})
export class PermissionModule {}

启动项目,我们就会看到数据库中多了几张表

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

添加模拟数据

接下来给每个模块添加一个新增数据的接口用于添加数据来观察它们之间的关系

permission 模块

新增 permission 数据很简单,直接保存前端传来的的namedesc字段即可

//permission.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
import { ApiException } from 'src/common/filter/http-exception/api.exception';
import { Repository } from 'typeorm';
import { CreatePermissionDto } from './dto/create-permission.dto';
import { Permission } from './entities/permission.entity';
@Injectable()
export class PermissionService {
  constructor(
    @InjectRepository(Permission)
    private permissionRepository: Repository<Permission>,
  ) {}
  async create(createPermissionDto: CreatePermissionDto) {
    const name = createPermissionDto.name;
    const existPermission = await this.permissionRepository.findOne({
      where: { name },
    });

    if (existPermission)
      throw new ApiException('权限字段已存在', ApiErrorCode.PERMISSSION_EXIST);
    return await this.permissionRepository.save(createPermissionDto);
  }
}


//create-user.dto.ts
export class CreatePermissionDto {
  name: string;
  desc: string;
}

然后我们可以调用http://localhost:3000/permission接口新增一些权限字段create,read,update,delete

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

新增完毕之后,permission 表如下

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

role 模块

新增 role 表中的数据和上面有所不同,因为它有一个关系字段permissions,因此需要导入 permission 实体,然后根据前端传来的权限字段 id(permissionIds)查询对应 permission 实体插入 role 表中

//role.module.ts
import { Module } from '@nestjs/common';
import { RoleService } from './role.service';
import { RoleController } from './role.controller';
import { Role } from './entities/role.entity';
import { Permission } from '../permission/entities/permission.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
  controllers: [RoleController],
  providers: [RoleService],
  imports: [TypeOrmModule.forFeature([Role, Permission])],
})
export class RoleModule {}
//create-role.dto.ts
export class CreateRoleDto {
  name: string;
  permissionIds: number[];
}
//role.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { CreateRoleDto } from './dto/create-role.dto';
import { Role } from './entities/role.entity';
import { Permission } from '../permission/entities/permission.entity';
import { ApiException } from 'src/common/filter/http-exception/api.exception';
import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
@Injectable()
export class RoleService {
  constructor(
    @InjectRepository(Role)
    private roleRepository: Repository<Role>,
    @InjectRepository(Permission)
    private permissionRepository: Repository<Permission>,
  ) {}
  async create(createRoleDto: CreateRoleDto) {
    //查询传入数组permissionIds的全部permission实体
    const permissions = await this.permissionRepository.find({
      where: {
        id: In(createRoleDto.permissionIds),
      },
    });
    const name = createRoleDto.name;
    const existRole = await this.roleRepository.findOne({
      where: { name },
    });

    if (existRole)
      throw new ApiException('角色已存在', ApiErrorCode.ROLE_EXIST);
    return this.roleRepository.save({ permissions, name });
  }
}

最后调用http://localhost:3000/role就能添加一些角色,分别添加超级管理员(create,read,update,delete),管理员(create,read,update),用户(read)

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

添加完成之后 role 表如下

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

同时我们可以查看role_permission_relation关系表来看一下它们的多对多关系

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

user 模块

user 模块和 role 模块逻辑差不多,需要查询 role 表,因此要引入 role 实体

//user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User } from './entities/user.entity';
import { Role } from '../role/entities/role.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
@Module({
  controllers: [UserController],
  providers: [UserService],
  imports: [
    TypeOrmModule.forFeature([User, Role]),
    JwtModule.register({ secret: process.env.JWT_SECRET }),
  ],
  exports: [UserService],
})
export class UserModule {}

在 user.service.ts 写对应逻辑

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';
import { Role } from '../role/entities/role.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { ApiException } from 'src/common/filter/http-exception/api.exception';
import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
    @InjectRepository(Role)
    private roleRepository: Repository<Role>,
  ) {}
  async create(createUserDto: CreateUserDto) {
    const { username, password, roleIds } = createUserDto;
    const existUser = await this.userRepository.findOne({
      where: { username },
    });

    if (existUser)
      throw new ApiException('用户已存在', ApiErrorCode.USER_EXIST);
    try {
      //查询数组roleIds对应所有role的实例
      const roles = await this.roleRepository.find({
        where: {
          id: In(roleIds),
        },
      });
      const newUser = await this.userRepository.create({
        username,
        password,
        roles,
      });
      await this.userRepository.save(newUser);
      return '注册成功';
    } catch (error) {
      throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }
  async findOne(username: string) {
    const user = await this.userRepository.findOne({
      where: { username },
    });

    if (!user)
      throw new ApiException('用户名不存在', ApiErrorCode.USER_NOTEXIST);
    return user;
  }
}

在 create-user.dto.ts 规定前端传来参数格式

//create-user.dto.ts
export class CreateUserDto {
  username: string;
  password: string;
  roleIds: number[];
}

创建两个用户分别为 admin 和 user1 分别赋予全部角色和用户角色

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

权限控制

我们可以通过自定义装饰器在调用接口之前传入一个权限数组,即必须拥有数组内的权限才能够调用此接口。所以可与创建一个全局的导航守卫,里面查询到该用户的所有权限然后与自定义装饰器传入的权限进行对比即可

现在 public 目录下新增一个@Permissions装饰器

//public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Public = () => SetMetadata('isPublic'true);

export const Permissions = (...permissions: string[]) =>
  SetMetadata('permissions', permissions);

然后在 permission 模块下创建一个导航守卫

nest g guard permission --no-spec --flat

同样的,这个守卫需要设置为全局守卫

//permission.module.ts
import { Module } from '@nestjs/common';
import { PermissionService } from './permission.service';
import { PermissionController } from './permission.controller';
import { Permission } from './entities/permission.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { APP_GUARD } from '@nestjs/core';
import { PermissionGuard } from '../permission/permission.guard';
@Module({
  controllers: [PermissionController],
  providers: [
    PermissionService,
    {
      provide: APP_GUARD,
      useClass: PermissionGuard,
    },
  ],
  imports: [TypeOrmModule.forFeature([Permission])],
})
export class PermissionModule {}

接下来我们开始写相关逻辑,在 user 中简单写一个测试接口,同时用我们刚写的自定义装饰器传入两个权限

//user.controller.ts
  @Post('test')
  @Permissions('create''read')
  test(@Body() testParams) {
    return this.userService.test(testParams);
  }

修改一下权限守卫,获取传入的元数据


//permission.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';

@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
      'permissions',
      [context.getClass(), context.getHandler()],
    );

    console.log(requiredPermissions);
    return true;
  }
}

在调用接口之前我们先把登录守卫修改一下,让其都放行,不然接口是调不通的,只需要修改auth.guard.ts让其都返回 true 即可

import {
  CanActivate,
  ExecutionContext,
  HttpException,
  HttpStatus,
  Injectable,
from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
import { ApiException } from 'src/common/filter/http-exception/api.exception';
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private jwtService: JwtService,
    private configService: ConfigService,
    private reflector: Reflector,
  ) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
      //即将调用的方法
      context.getHandler(),
      //controller类型
      context.getClass(),
    ]);



    //这里暂时都放行
    return true;



    if (isPublic) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) throw new ApiException('验证不通过', ApiErrorCode.FORBIDDEN);
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secretthis.configService.get('JWT_SECRET'),
      });
      request['user'] = payload;
    } catch {
      throw new ApiException(
        '登录状态已过期,请重新登录',
        ApiErrorCode.LOGIN_EXPIRE,
      );
    }

    return true;
  }
  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

调用 user/test 接口查看我们的打印结果

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

可以发现已经获取到装饰器传来的权限数组了,接下来我们要做的是查询用户的所有权限与之对比,看是否包含装饰器传来的数组,包含的话则放行否则抛出没有权限的错误给前端。

因为需要查询该用户的权限字段,所以在user.service.ts写一个查询权限字段名的方法,这里模拟查询admin用户的权限

//user.service.ts
  async findPermissionNames() {
    const user = await this.userRepository.findOne({
      where: { username'admin' },
      relations: ['roles''roles.permissions'],
    });
    if (user) {
      const permissions = user.roles.flatMap((role) => role.permissions);
      const permissionNames = permissions.map((item) => item.name);

      return [...new Set(permissionNames)];
    } else {
      return [];
    }
  }

因为这个方法要提供给权限守卫使用,所以需要暴露出去,并在permission.module.ts中导入

//...
import { UserModule } from 'src/user/user.module';
@Module({
  //...
  imports: [UserModule, TypeOrmModule.forFeature([Permission])],
})
export class PermissionModule {}

然后在导航守卫中引入,因为获取的是异步的,因此守卫做了一点修改

//permission.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserService } from 'src/user/user.service';
@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private userServicese: UserService,
  ) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
      'permissions',
      [context.getClass(), context.getHandler()],
    ) || [];
    const permissionNames = await this.userServicese.findPermissionNames();
    console.log(requiredPermissions);
    console.log(permissionNames);

    return true;
  }
}

调用user/test接口即可获得接口权限字段与该用户拥有的权限字段

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

最后看该角色权限是否包含接口权限即可

//permission.guard.ts
//...
const isContainedPermission = requiredPermissions.every((item) =>
  permissionNames.includes(item),
);
if (!isContainedPermission) {
  throw new ApiException('权限不足', ApiErrorCode.Forbidden);
}
return true;

完善一下

auth.guard.ts我们已经将 jwt 验证的信息放入了 request[‘user’]中了

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

所以可以在权限守卫中获取到登录用户的信息,然后调用 user 模块中的findPermissionNames即可获取当前登录人的权限集合,权限守卫修改如下

//permission.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserService } from 'src/user/user.service';
import { Request } from 'express';
import { ApiException } from 'src/common/filter/http-exception/api.exception';
import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private userServicese: UserService,
  ) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    interface CusRequest extends Request {
      user?: any;
    }
    const request: CusRequest = context.switchToHttp().getRequest();
    const requiredPermissions =
      this.reflector.getAllAndOverride<string[]>('permissions', [
        context.getClass(),
        context.getHandler(),
      ]) || [];

    if (requiredPermissions.length === 0return true;
    const [, token] = request.headers.authorization?.split(' ') ?? [];

    const permissionNames = await this.userServicese.findPermissionNames(
      token,
      request.user,
    );

    const isContainedPermission = requiredPermissions.every((item) =>
      permissionNames.includes(item),
    );
    if (!isContainedPermission) {
      throw new ApiException('权限不足', ApiErrorCode.Forbidden);
    }
    return true;
  }
}

findPermissionNames获取到传来的用户信息,将查询的 username 替换即可

//user.service.ts
  async findPermissionNames(token: string, userInfo) {
    const user = await this.userRepository.findOne({
      where: { username: userInfo.username },
      relations: ['roles''roles.permissions'],
    });
    if (user) {
      const permissions = user.roles.flatMap((role) => role.permissions);
      const permissionNames = permissions.map((item) => item.name);
      return [...new Set(permissionNames)];
    } else {
      return [];
    }
  }

最后调用登录接口登录只有一个用户角色的 user1 用户,拿到它的 token 调用user/test接口

Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

因为角色用户只有read权限,而 test 接口需要create, read权限,所以被拦截了,我们将接口改成只需要read权限再试一下

//user.controller.ts
  @Post('test')
  @Permissions('read')
  test(@Body() testParams) {
    return this.userService.test(testParams);
  }
Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现
image.png

此时我们发现请求成功了!

到这里基于RBAC权限控制基本完成了,但是还有很多不足之处,比如不需要每次调用接口都查询数据库中用户的权限,我们只需要在登录时查询一次缓存在Redis中即可,这部分后续文章会进行介绍,欢迎点赞收藏加关注~

源码地址

Vue3 + Nest 实现权限管理系统 后端


原文始发于微信公众号(web前端进阶):Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/231938.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!