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 实现权限管理系统 后端

相关文章
|
2月前
|
缓存 监控 安全
构建高效后端系统的最佳实践
本文将深入探讨如何构建一个高效的后端系统,从设计原则、架构选择到性能优化等方面详细阐述。我们将结合实际案例和理论分析,帮助读者了解在构建后端系统时需要注意的关键点,并提供一些实用的建议和技巧。
42 2
|
1月前
|
存储 缓存 监控
构建高效后端系统的关键要素
【10月更文挑战第12天】 本文将探讨如何通过合理的架构设计、数据库优化、缓存策略和性能调优等措施,构建一个稳定、高效的后端系统。我们将分享一些实用的技巧和最佳实践,帮助开发者提升后端系统的性能和用户体验。
33 1
|
1月前
|
JavaScript 前端开发 API
vue获取图片的blob传给django后端
vue获取图片的blob传给django后端
35 4
|
1月前
|
存储 缓存 测试技术
构建高效后端系统的关键策略##
【10月更文挑战第2天】 在当今数字化时代,后端系统的性能和可扩展性对于任何技术驱动的企业都至关重要。本文将深入探讨如何通过优化数据库设计、实施缓存机制、采用微服务架构以及自动化测试等措施,构建一个既高效又可靠的后端系统。我们将从基础的代码优化开始,逐步讨论到架构级别的改进,最终目标是提供一个全面的指导方案,帮助开发者和企业提升其后端系统的性能和可维护性。 ##
56 2
|
1月前
|
数据可视化 测试技术 Linux
基于Python后端构建多种不同的系统终端界面研究
【10月更文挑战第10天】本研究探讨了利用 Python 后端技术构建多样化系统终端界面的方法,涵盖命令行界面(CLI)、图形用户界面(GUI)及 Web 界面。通过分析各种界面的特点、适用场景及关键技术,展示了如何使用 Python 标准库和第三方库(如 `argparse`、`click`、`Tkinter` 和 `PyQt`)实现高效、灵活的界面设计。旨在提升用户体验并满足不同应用场景的需求。
|
1月前
|
Web App开发 JavaScript API
构建高效后端系统:Node.js与Express框架的实践之路
【9月更文挑战第37天】在数字化时代的浪潮中,后端开发作为技术架构的核心,承载着数据处理和业务逻辑的重要职责。本文将深入探讨如何利用Node.js及其强大的Express框架来搭建一个高效、可扩展的后端系统。我们将从基础概念讲起,逐步引导读者理解并实践如何设计、开发和维护一个高性能的后端服务。通过实际代码示例和清晰的步骤说明,本文旨在为初学者和有经验的开发者提供一个全面的指南,帮助他们在后端开发的旅途上走得更远。
48 3
|
1月前
|
JavaScript 前端开发
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
本文提供了一个Vue 3教程,讲解了如何使用axios库手动从后端获取数据,包括安装axios、配置后端访问地址、编写路由地址、发起HTTP请求以及在组件中读取和打印响应数据的步骤。
328 0
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
|
2月前
|
关系型数据库 Java 数据库
探索后端技术:构建高效系统的秘诀
在当今信息技术飞速发展的时代,后端开发作为软件架构中的核心部分,其重要性不言而喻。本文将深入探讨后端技术的各个方面,从基础概念到高级应用,帮助读者全面了解后端开发的世界,揭示构建高效系统的秘诀。
|
1月前
|
前端开发 JavaScript Java
导出excel的两个方式:前端vue+XLSX 导出excel,vue+后端POI 导出excel,并进行分析、比较
这篇文章介绍了使用前端Vue框架结合XLSX库和后端结合Apache POI库导出Excel文件的两种方法,并对比分析了它们的优缺点。
242 0
|
1月前
|
前端开发 JavaScript 小程序
前端uni开发后端用PHP的圈子系统该 如何做源码?
圈子系统系统基于TP6+Uni-app框架开发;客户移动端采用uni-app开发,管理后台TH6开发。系统支持微信公众号端、微信小程序端、H5端、PC端多端账号同步,可快速打包生成APP

热门文章

最新文章

下一篇
无影云桌面