Skip to content

NestJS 使用 ACL 权限控制

什么是 ACL

ACL 是 用户直接绑定权限.没有角色的概念

数据库

  • user 表
sql
CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `createTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
  • permission 表
sql
CREATE TABLE `permission` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `desc` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `createTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_permission` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
  • user_permission_relation 表
sql
CREATE TABLE `user_permission_relation` (
  `userId` int NOT NULL,
  `permissionId` int NOT NULL,
  PRIMARY KEY (`userId`,`permissionId`)
) 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")
@UseGuards(AaaGuard)
export class AaaController {
  constructor(private readonly aaaService: AaaService) {}
  @Get()
  @SetMetadata("permission", ["query_aaa", "create_aaa"]) // 添加访问接口的权限
  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);
      }
    }
  }
}

权限守卫

注意

第一次访问接口的时候,会先走全局守卫,拿到 userId 后,再走权限守卫,权限守卫会先去 redis 中查找,如果找不到,就会去数据库中查找,并把结果存到 redis 中,下次访问的时候,就会直接走 redis,不用再去数据库中查找了

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.get<string[]>(
      "permission",
      context.getHandler()
    );
    // 如果是绑定到整个路由
    // 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}_permissions`
    );
    if (!userPermissionsName || userPermissionsName.length === 0) {
      // 1. 查出这个用户拥有的权限Id
      const userPermissionsId =
        await this.prismadbService.user_permission_relation.findMany({
          where: {
            userId: request.userId,
          },
          select: {
            permissionId: true,
          },
        });
      // 2. 查出这个权限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);
      // 3. 存到redis中
      this.redisService.setList(
        `userId_${request.userId}_permissions`,
        userPermissionsName,
        60 * 30
      ); // 过期时间30分钟
    }
    // 判断返回
    const result = permissions.every((item) => {
      return userPermissionsName.includes(item);
    });

    return result;
  }
}