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

简介: RBAC(Role Based Access Control)是基于角色的权限控制,简单来说就是给用户赋予一些角色,那么该用户就会拥有这些角色的所有权限。接下来我们就用 NestJS 来实现基于 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({
   
   
    length: 50,
  })
  name: string;

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

  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;
}

一个权限可以赋给多个角色,同时一个角色也可以有多个权限,因此它们是多对多(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({
   
   
    length: 20,
  })
  name: string;

  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;

  @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({
   
    length: 30 })
  username: string; //用户名
  @Column({
   
    nullable: true })
  nickname: string; //昵称
  @Column()
  password: string; //密码
  @Column({
   
    nullable: true })
  avatar: string; //头像
  @Column({
   
    nullable: true })
  email: string; //邮箱

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

  @Column({
   
    nullable: true })
  salt: string;
  @Column({
   
    type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  create_time: Date;

  @Column({
   
    type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  update_time: Date;
  @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 {
   
   }

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

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

image.png

新增完毕之后,permission 表如下

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)

image.png

添加完成之后 role 表如下

image.png

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

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 分别赋予全部角色和用户角色

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, {
   
   
        secret: this.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 接口查看我们的打印结果

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接口即可获得接口权限字段与该用户拥有的权限字段

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']中了

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 === 0) return 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接口

image.png

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

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

image.png

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

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

源码地址

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

相关文章
|
5天前
|
人工智能 运维 监控
构建高性能微服务架构:现代后端开发的挑战与策略构建高效自动化运维系统的关键策略
【2月更文挑战第30天】 随着企业应用的复杂性增加,传统的单体应用架构已经难以满足快速迭代和高可用性的需求。微服务架构作为解决方案,以其服务的细粒度、独立性和弹性而受到青睐。本文将深入探讨如何构建一个高性能的微服务系统,包括关键的设计原则、常用的技术栈选择以及性能优化的最佳实践。我们将分析微服务在处理分布式事务、数据一致性以及服务发现等方面的挑战,并提出相应的解决策略。通过实例分析和案例研究,我们的目标是为后端开发人员提供一套实用的指南,帮助他们构建出既能快速响应市场变化,又能保持高效率和稳定性的微服务系统。 【2月更文挑战第30天】随着信息技术的飞速发展,企业对于信息系统的稳定性和效率要求
|
5天前
|
JSON JavaScript 前端开发
vue的 blob文件下载文件时,后端自定义异常,并返回json错误提示信息,前端捕获信息并展示给用户
vue的 blob文件下载文件时,后端自定义异常,并返回json错误提示信息,前端捕获信息并展示给用户
|
5天前
|
jenkins 持续交付
基于Jeecgboot前后端分离的平台后端系统采用jenkins发布
基于Jeecgboot前后端分离的平台后端系统采用jenkins发布
|
5天前
|
JSON JavaScript Java
从前端Vue到后端Spring Boot:接收JSON数据的正确姿势
从前端Vue到后端Spring Boot:接收JSON数据的正确姿势
26 0
|
5天前
|
API 开发者 UED
构建高效微服务架构:后端开发的新趋势移动应用与系统:开发与优化的艺术
【4月更文挑战第30天】 随着现代软件系统对可伸缩性、灵活性和敏捷性的日益需求,传统的单体应用架构正逐渐向微服务架构转变。本文将探讨微服务架构的核心概念,分析其优势,并着重讨论如何利用最新的后端技术栈实现一个高效的微服务系统。我们将涵盖设计模式、服务划分、数据一致性、服务发现与注册、API网关以及容器化等关键技术点,为后端开发者提供一份实操指南。 【4月更文挑战第30天】 在数字化时代的浪潮中,移动应用和操作系统的紧密交织已成为日常生活和商业活动的基石。本文将深入探讨移动应用开发的关键技术、跨平台开发工具的选择以及移动操作系统的架构和性能优化策略。通过分析当前移动应用开发的挑战与机遇,我们将
|
5天前
|
JavaScript 前端开发 数据安全/隐私保护
【好用】推荐10套后端管理系统前端模板
选择合适的模板可以大大提高开发效率,减少重复劳动,让开发者能够专注于业务逻辑的实现和功能的优化。开发者应根据项目的具体需求、团队的技术栈熟悉度以及产品的长远规划来选择最合适的模板,问题来了,这10款模板你更喜欢用哪个呢,欢迎交流。
|
5天前
|
机器学习/深度学习 Kubernetes 微服务
后端技术发展及其在高性能系统中的应用研究
后端技术发展及其在高性能系统中的应用研究
18 0
|
5天前
|
JSON 前端开发 Java
管理系统总结(前端:Vue-cli, 后端Jdbc连接mysql数据库,项目部署tomcat里)
管理系统总结(前端:Vue-cli, 后端Jdbc连接mysql数据库,项目部署tomcat里)
|
5天前
|
安全 关系型数据库 MySQL
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
28 3
|
5天前
|
JavaScript
【vue】如何压缩图片之后传给后端
【vue】如何压缩图片之后传给后端
13 0