Skip to content

Nest 使用 RBAC 权限控制

RBAC

RBAC(Role-Based Access Control,基于角色的访问控制)是一种访问控制方法,它将权限与角色关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。

实现思路

  • 定义权限表、角色表、用户表、角色权限关系表、用户角色关系表
  • 在控制器上使用装饰器定义权限,在守卫中获取权限,与用户角色关系表中的角色进行对比,如果存在则放行,否则抛出异常

数据库

  • permission 权限表
sql

CREATE TABLE `permission` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `createTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
  • role 角色表
sql
CREATE TABLE `role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `createTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
  • user
sql
CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `password` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `createTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
  • role_permission_relation
sql
CREATE TABLE `role_permission_relation` (
  `roleId` int NOT NULL,
  `permissionId` int NOT NULL,
  PRIMARY KEY (`roleId`,`permissionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
  • user_role_relation
sql
CREATE TABLE `user_role_relation` (
  `userId` int NOT NULL,
  `roleId` int NOT NULL,
  PRIMARY KEY (`userId`,`roleId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

代码

控制器

  • 这里我加在了控制器上,也可以加在方法上
ts
import { Controller, Get, UseGuards, SetMetadata } from "@nestjs/common";
import { AaaService } from "./aaa.service";
import { AaaGuard } from "./aaa.guard";
@Controller("aaa")
@SetMetadata("permissions", ["查询aaa"])
@UseGuards(AaaGuard)
export class AaaController {
  constructor(private readonly aaaService: AaaService) {}
  @Get()
  findAll() {
    return {
      data: this.aaaService.findAll(),
    };
  }
}

全局守卫

他的作用就是验证 jwt,并把解析出来的信息赋值给 request,这样在下一步守卫中就可以拿到 userId 了

ts
import {
  CanActivate,
  ExecutionContext,
  Inject,
  Injectable,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { GlobalCheckException } from "./globalcheck.exception";
import { ConfigService } from "@nestjs/config";
import { JwtServiceAll } from "../modules/jwt/jwt.service";
@Injectable()
export class GlobalGuard implements CanActivate {
  @Inject()
  private configService: ConfigService;
  @Inject()
  private jwtService: JwtServiceAll;
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    //权限验证
    const request = context.switchToHttp().getRequest();
    // 白名单
    const whiteList = this.configService.get("perm").router.whiteList;
    // 验证
    if (whiteList.includes(request.url)) {
      return true;
    } else {
      // 判断
      const authorization = request.header("Authorization") || "";

      const bearer = authorization.split(" ");

      if (!bearer || bearer.length < 2) {
        const result = JSON.stringify([{ message: "请输入token" }]);
        throw new GlobalCheckException(result);
      }

      const token = bearer[1];
      try {
        const info = this.jwtService.verifyToken(token);
        // 我这里是解析出来后把userId赋值给request 这样控制器就拿到了
        (request as any).userId = info.id; // 换成你自己的payload对应字段
        return true;
      } catch (e) {
        const result = JSON.stringify([{ message: "token 错误" }]);
        throw new GlobalCheckException(result);
      }
    }
  }
}

权限守卫

  • aaa.guard.ts
ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  Inject,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { Reflector } from "@nestjs/core";
import { PrismadbService } from "../prisma/prisma.service";
import { RedisService } from "../redis/redis.service";
@Injectable()
export class AaaGuard implements CanActivate {
  @Inject()
  private readonly redisService: RedisService;
  @Inject()
  private readonly prismadbService: PrismadbService;
  @Inject(Reflector)
  private readonly reflector: Reflector;
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 获取到设定的权限
    const permissions = this.reflector.getAllAndOverride<string[]>(
      "permissions",
      [context.getHandler(), context.getClass()]
    );
    // 通过userId获取到用户角色
    // 权限验证 全局守卫会给你useId
    const request = context.switchToHttp().getRequest();
    // 1. 先去redis中查找 2. 找不到就走数据库 3. 数据库取出来存到redis中
    let userPermissionsName: any = await this.redisService.getList(
      `userId_${request.userId}_roles`
    );
    if (!userPermissionsName || userPermissionsName.length === 0) {
      console.log("进来了");
      // 1. 查出这个用户拥有的角色
      const RoleId = await this.prismadbService.user_role_relation.findMany({
        where: {
          userId: request.userId,
        },
        select: {
          roleId: true,
        },
      });
      // 2. 查询出这个角色拥有的权限
      const userPermissionsId =
        await this.prismadbService.role_permission_relation.findMany({
          where: {
            roleId: {
              in: RoleId.map((item) => item.roleId),
            },
          },
          select: {
            permissionId: true,
          },
        });
      console.log(userPermissionsId);
      // 3. 查出这个权限Id对应的权限
      let userPermissions = await this.prismadbService.permission.findMany({
        where: {
          id: {
            in: userPermissionsId.map((item) => item.permissionId),
          },
        },
        select: {
          name: true,
        },
      });
      userPermissionsName = userPermissions.map((item) => item.name);
      console.log("最后结果");
      console.log(userPermissionsName);
      // 3. 存到redis中
      this.redisService.setList(
        `userId_${request.userId}_roles`,
        userPermissionsName,
        60 * 30
      ); // 过期时间30分钟
    }
    // 判断返回
    const result = permissions.every((item) => {
      return userPermissionsName.includes(item);
    });

    return result;
  }
}