Skip to content

NEST 全局九件套

九件套架构

  • 新建一个 src/common 文件夹
bash

── src
   ├── app.module.ts
   ├── common
   ├── guard_pass.decorator.ts 守卫放行装饰器
   ├── bypass.decorator.ts 拦截器放行装饰器
   ├── globalcheck.exception.ts 管道抛出的异常
   ├── globalguard.exception.ts 守卫抛出的异常
   ├── global_check.filter.ts 捕捉管道抛出的异常
   ├── global_guard_check.filter.ts 捕捉守卫抛出的异常
   ├── global_unauth.filter.ts 捕捉登录策略抛出的异常
   ├── global.filter.ts 全局过滤器 捕捉其他全局异常
   ├── global.guard.ts 全局守卫 验证jwt 等等
   ├── global.interceptor.ts 全局拦截器 返回格式化数据
   ├── rbac.guard.ts 权限验证守卫(可以不用 看需求)
   ├── rbac.decorator.ts 权限验证装饰器(可以不用 看需求)
   └── global.pipe.ts 全局管道 验证参数等等
   ├── main.ts

bypass.decorator.ts

ts
import { SetMetadata } from "@nestjs/common";
// 加载环境变量
import dotenv from "dotenv";
dotenv.config();

export const BYPASS_KEY = process.env.BYPASS_KEY;

export function Bypass() {
  return SetMetadata(BYPASS_KEY, true);
}
  • 使用在控制器前面加上@Bypass 这样拦截器就放行了
bash
  @Get('exportexcel2')
  @Bypass()
  @ByGuardpass()
  async exportexcel2(@Res() res: any) {
    // 这里应该上传成功后,返回一个url,然后通过url获取到excel文件
    const result = await this.testdemoService.exportexcel2();
    console.log(result);
    return res.send(result);
  }

global_check.filter.ts

  • 管道使用 一般用于参数验证,或者判断用户权限 code 403
ts
import { ExceptionFilter, Catch, ArgumentsHost, Inject } from "@nestjs/common";

import { GlobalCheckException } from "./globalcheck.exception";

// 增加日志模块
import { Logger } from "winston";
import { getReqMainInfo } from "../commonModules/utils/tools";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";

@Catch(GlobalCheckException)
export class GlobalExceptionsCheckFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
  ) {}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = 200;
    // 错误内容
    const message =
      exception instanceof GlobalCheckException
        ? exception.message
        : exception.stack;
    // 记录日志(错误消息,错误码,请求信息等)
    this.logger.error(message, {
      status: 403,
      req: getReqMainInfo(request),
      // stack: exception.stack,
    });
    response.status(status).json({
      code: 403,
      data: message,
      message: "操作失败",
    });
  }
}

global_guard_check.filter.ts

  • 守卫使用 一般用于 token 验证,code 401
ts
import { ExceptionFilter, Catch, ArgumentsHost, Inject } from "@nestjs/common";

import { GlobalGuardException } from "./globalguard.exception";

// 增加日志模块
import { Logger } from "winston";
import { getReqMainInfo } from "../commonModules/utils/tools";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";

@Catch(GlobalGuardException)
export class GlobalGuardCheckFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
  ) {}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = 200;
    // 错误内容
    const message =
      exception instanceof GlobalGuardException
        ? exception.message
        : exception.stack;

    // 记录日志(错误消息,错误码,请求信息等)
    this.logger.error(message, {
      status: 401,
      req: getReqMainInfo(request),
      // stack: exception.stack,
    });
    response.status(status).json({
      code: 401,
      data: message,
      message: "操作失败",
    });
  }
}

global_unauth.filter.ts

ts
import { ExceptionFilter, Catch, ArgumentsHost, Inject } from "@nestjs/common";

import { Injectable, UnauthorizedException } from "@nestjs/common";

// 增加日志模块
import { Logger } from "winston";
import { getReqMainInfo } from "../commonModules/utils/tools";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";

@Catch(UnauthorizedException)
export class UnauthorizedExceptionFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
  ) {}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status = 200;

    // 依据你自己的路由 来写错误信息
    let message =
      exception instanceof UnauthorizedException
        ? exception.message
        : exception.stack;
    if (message == "Unauthorized") {
      message = "用户名或者密码不能为空";
    }
    // 记录日志(错误消息,错误码,请求信息等)
    this.logger.error(message, {
      status: 403,
      req: getReqMainInfo(request),
      // stack: exception.stack,
    });
    response.status(status).json({
      code: 403,
      data: message,
      message: "操作失败",
    });
  }
}

global.filter.ts

  • 托底了 剩下的异常 都靠这个捕获了.也可以把发短信,发邮件,发消息,发微信等操作都写在这里
ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Inject,
} from "@nestjs/common";

// 增加日志模块
import { Logger } from "winston";
import { getReqMainInfo } from "../commonModules/utils/tools";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";

@Catch()
export class GlobalExceptionsFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
  ) {}

  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof HttpException ? exception.message : exception.stack;

    // 记录日志(错误消息,错误码,请求信息等)
    this.logger.error(message, {
      status: status,
      req: getReqMainInfo(request),
      // stack: exception.stack,
    });

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: message,
      name: "出问题了",
    });
  }
}

global.guard.ts(env 版本)

  • 主要验证 token jwt
ts
import {
  CanActivate,
  ExecutionContext,
  Inject,
  Injectable,
} from "@nestjs/common";
import { GlobalGuardException } from "./globalguard.exception";
import { Reflector } from "@nestjs/core";
import { GUARD_PASS_KEY } from "./guard_pass.decorator";
import { JwtAllService } from "../commonModules/jwt/jwt.service";
@Injectable()
export class GlobalGuard implements CanActivate {
  @Inject()
  private reflector: Reflector;

  @Inject()
  private jwtService: JwtAllService;
  async canActivate(context: ExecutionContext): Promise<boolean> {
    let isPublic = false;
    //权限验证
    const request = context.switchToHttp().getRequest();
    // 放行
    // 写一个装饰器不拦截 获取装饰器 bypass
    const by_pass_guard = this.reflector.get<boolean>(
      GUARD_PASS_KEY,
      context.getHandler()
    );
    // 验证装饰器
    if (by_pass_guard) {
      isPublic = true;
    }
    // 放行
    if (isPublic) {
      return true;
    } else {
      // 判断
      const authorization = request.header("Authorization") || "";

      const bearer = authorization.split(" ");

      if (!bearer || bearer.length < 2) {
        throw new GlobalGuardException("登录token错误");
      }

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

global.guard.ts(yml 版本)

  • 主要验证 token jwt
ts
import {
  CanActivate,
  ExecutionContext,
  Inject,
  Injectable,
} from "@nestjs/common";
import { GlobalGuardException } from "./globalguard.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;
  async canActivate(context: ExecutionContext): Promise<boolean> {
    let isPublic = false;
    //权限验证
    const request = context.switchToHttp().getRequest();
    // 白名单
    const whiteList = this.configService.get("perm").router.whiteList;
    // 如果没有就放行
    if (whiteList.length == 0) {
      isPublic = true;
    } else {
      // 验证
      let item = { path: request.url, method: request.method };
      isPublic = whiteList.some((content) => {
        if (content.path === item.path && content.method === item.method) {
          return true;
        }
      });
    }
    if (isPublic) {
      return true;
    } else {
      // 判断
      const authorization = request.header("Authorization") || "";

      const bearer = authorization.split(" ");

      if (!bearer || bearer.length < 2) {
        throw new GlobalGuardException("登录token错误");
      }

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

global.interceptor.ts

  • 全局拦截器(env 版本)
ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Inject,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap, map } from "rxjs/operators";

import { Reflector } from "@nestjs/core";
// 增加日志模块
import { Logger } from "winston";
import { getReqMainInfo } from "../commonModules/utils/tools";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";

import { BYPASS_KEY } from "./bypass.decorator";

@Injectable()
export class GlobalInterceptor implements NestInterceptor {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
  ) {}

  @Inject()
  private reflector: Reflector;

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    let isPublic = false;
    //权限验证
    const request = context.switchToHttp().getRequest();
    // 写一个装饰器不拦截 获取装饰器 bypass
    const bypass = this.reflector.get<boolean>(
      BYPASS_KEY,
      context.getHandler()
    );

    // 装饰器 bypass 则放行
    if (bypass) {
      isPublic = true;
    }

    if (isPublic) {
      return next.handle().pipe(
        map((data) => {
          // 记录请求日志
          this.logger.info("response", {
            responseData: data,
            req: getReqMainInfo(request),
          });
          return data;
        })
      );
    } else {
      return next.handle().pipe(
        map((data) => {
          console.log("After全局...");

          // 剩下的是全局返回
          // 控制器里面就可以返回 {code:200,data:xxx}
          let resultmessage = "";
          let resultcode = data.code || 200;
          if (resultcode == 200) {
            resultmessage = "操作成功";
          } else {
            resultmessage = "操作失败";
          }
          // 记录请求日志
          this.logger.info("response", {
            responseData: data,
            req: getReqMainInfo(request),
          });
          return {
            code: resultcode,
            message: resultmessage,
            data,
          };
        })
      );
    }
  }
}
  • 全局拦截器(ysml 版本)
ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Inject,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap, map } from "rxjs/operators";
import { ConfigService } from "@nestjs/config";
import { BYPASS_KEY } from "./bypass.decorator";
import { Reflector } from "@nestjs/core";
@Injectable()
export class GlobalInterceptor implements NestInterceptor {
  @Inject()
  private configService: ConfigService;
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log("Before...");
    // 写一个白名单不拦截
    let isPublic = false;
    //权限验证
    const request = context.switchToHttp().getRequest();
    // 白名单
    const whiteList = this.configService.get("noglobalinterceptor").router
      .whiteList;
    // 如果没有就放行
    if (whiteList.length == 0) {
      isPublic = true;
    } else {
      // 验证
      let item = { path: request.url, method: request.method };
      isPublic = whiteList.some((content) => {
        if (content.path === item.path && content.method === item.method) {
          return true;
        }
      });
    }
    const bypass = this.reflector.get<boolean>(
      BYPASS_KEY,
      context.getHandler()
    );
    // 装饰器 bypass 则放行
    if (bypass) {
      isPublic = true;
    }
    if (isPublic) {
      return next.handle();
    } else {
      return next.handle().pipe(
        map((data) => {
          console.log("After全局...");
          let { code, message, ...result } = data;
          // 剩下的是全局返回
          // 控制器里面就可以返回 {code:200,data:xxx}
          let resultmessage = "";
          let resultcode = code || 200;
          if (resultcode == 200) {
            resultmessage = "操作成功";
          } else {
            resultmessage = "操作失败";
          }
          console.log(result.data);
          return {
            code: resultcode,
            message: resultmessage,
            data: result.data,
          };
        })
      );
    }
  }
}

global.pipe.ts

  • 全局管道 验证参数 抛出异常
ts
import { PipeTransform, Injectable, ArgumentMetadata } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";
// 引入我自己验证的异常类
import { GlobalCheckException } from "./globalcheck.exception";
@Injectable()
export class GlobalPipe implements PipeTransform {
  // 校验类型
  private toValidate(metatype: Function): boolean {
    // 其他类型不验证
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
  async transform(value: any, metadata: ArgumentMetadata) {
    const { metatype } = metadata;

    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    // 变成对象,把规则和值 组合成验证的对象
    const object = plainToInstance(metatype, value);
    // 验证 errors 是个数组
    const errors = await validate(object);

    if (errors.length > 0) {
      // 第一种只返回第一个
      if (errors.length == 1) {
        for (let arr in errors[0].constraints) {
          const result = `${errors[0].constraints[arr]}`;
          throw new GlobalCheckException(result);
        }
      }

      // 第二种返回所有
      let result: string[] = [];
      errors.forEach((item) => {
        for (let arr in item.constraints) {
          result.push(item.constraints[arr]);
        }
      });
      throw new GlobalCheckException(result.join("|"));
    }
    return value;
  }
}

globalcheck.exception.ts

ts
export class GlobalCheckException {
  message: string;
  constructor(message: string) {
    this.message = message;
  }
}

globalguard.exception.ts

ts
export class GlobalGuardException {
  message: string;
  constructor(message: string) {
    this.message = message;
  }
}

guard_pass.decorator.ts

ts
import { SetMetadata } from "@nestjs/common";
// 加载环境变量
import dotenv from "dotenv";
dotenv.config();

export const GUARD_PASS_KEY = process.env.GUARD_PASS_KEY;

export function ByGuardpass() {
  return SetMetadata(GUARD_PASS_KEY, true);
}
  • 使用在控制器前面加上@ByGuardpass
bash
  @ByGuardpass()
  @Post('init')
  async init() {
    await this.userService.init();
    return '初始化数据成功';
  }

rbac.decorator.ts

  • 权限装饰器
ts
import { SetMetadata } from "@nestjs/common";

export function RBAC(arr: any[]) {
  return SetMetadata("permissions", arr);
}

rule.guard.ts

  • 这里面涉及到 redis 和数据库换成你自己对应的 rbac 表名
ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  Inject,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { Reflector } from "@nestjs/core";
import { PrismadbService } from "../commonModules/prisma/prisma.service";
import { RedisService } from "../commonModules/redis/redis.service";
import { GlobalGuardException } from "./globalguard.exception";
@Injectable()
export class RuleGuard 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()]
    );
    console.log(permissions);
    // 通过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: any = await this.prismadbService.user_roles.findMany({
        where: {
          user_id: request.userId,
        },
        select: {
          role_id: true,
        },
      });
      // 2. 查询出这个角色拥有的权限
      const userPermissionsId: any =
        await this.prismadbService.role_permissions.findMany({
          where: {
            role_id: {
              in: RoleId.map((item) => item.role_id),
            },
          },
          select: {
            permission_id: true,
          },
        });
      console.log(userPermissionsId);
      // 3. 查出这个权限Id对应的权限
      let userPermissions = await this.prismadbService.permissions.findMany({
        where: {
          id: {
            in: userPermissionsId.map((item) => item.permission_id),
          },
        },
        select: {
          code: true,
        },
      });
      userPermissionsName = userPermissions.map((item) => item.code);
      console.log("最后结果");
      console.log(userPermissionsName);
      // 3. 存到redis中
      this.redisService.setlist(
        `userId_${request.userId}_roles`,
        userPermissionsName,
        60 * 30
      ); // 过期时间30分钟
    }
    // 判断返回
    const result = permissions.some((item) => {
      return userPermissionsName.includes(item);
    });

    if (result) {
      return true;
    } else {
      throw new GlobalGuardException("权限不足");
    }
  }
}
  • 使用
ts
  // 测试权限分级
  @UseGuards(RuleGuard)
  @RBAC(['guest'])
  @Post('rbac')
  async rbac(@Body() body: any) {
    console.log('----');
    console.log(body);
    console.log('-------');
    return '测试';
  }
}

app.module.ts

ts
import { Module } from "@nestjs/common";
// 配置文件
import { ConfigModule } from "@nestjs/config";
// 引入测试模块
import { TestprojectModule } from "./moduels/testproject/testproject.module";
import { PrismadbModule } from "./moduels/prisma/prisma.module";
import { RedisModule } from "./moduels/redis/redis.module";
import { AuthModule } from "./moduels/auth/auth.module";
import { JwtAllModule } from "./moduels/jwt/jwt.module";
// 引入九件套
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE, APP_GUARD } from "@nestjs/core";
import { GlobalExceptionsFilter } from "./common/global.filter";
import { GlobalExceptionsCheckFilter } from "./common/global_check.filter";
import { GlobalGuardCheckFilter } from "./common/global_guard_check.filter";
import { UnauthorizedExceptionFilter } from "./common/global_unauth.filter";
import { GlobalInterceptor } from "./common/global.interceptor";
import { GlobalPipe } from "./common/global.pipe";
import { GlobalGuard } from "./common/global.guard";
import { GlobalWinstonModule } from "./moduels/winston/winston.module";
// 加载配置文件,这里需要引入dotenv
import * as dotenv from "dotenv";
const envPath = `.env.${process.env.NODE_ENV || "development"}`;
@Module({
  imports: [
    // 修改配置
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: envPath,
      // 这里新增.env的文件解析
      load: [() => dotenv.config({ path: ".env" })],
    }),
    // 引入测试模块
    TestprojectModule,
    // 引入数据库模块
    PrismadbModule,
    // 引入redis模块
    RedisModule,
    // 登录策略模块
    AuthModule,
    // 引入jwt模块
    JwtAllModule,
    // 引入日志模块
    GlobalWinstonModule.forRoot(),
  ],
  controllers: [],
  providers: [
    // 全局异常过滤器(他负责兜底 处理其他异常)
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionsFilter,
    },
    // 检查管道过滤器
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionsCheckFilter,
    },
    // 守卫过滤器
    {
      provide: APP_FILTER,
      useClass: GlobalGuardCheckFilter,
    },
    // 策略过滤器
    {
      provide: APP_FILTER,
      useClass: UnauthorizedExceptionFilter,
    },
    // 全局统一格式拦截器
    {
      provide: APP_INTERCEPTOR,
      useClass: GlobalInterceptor,
    },
    // 全局管道
    {
      provide: APP_PIPE,
      useClass: GlobalPipe,
    },
    // 全局守卫
    {
      provide: APP_GUARD,
      useClass: GlobalGuard,
    },
  ],
})
export class AppModule {}

utils/tools

  • utils/tools
ts
interface RequestInfo {
  method: string;
  url: string;
  headers: Record<string, string>;
  ip: string;
  userAgent?: string;
  timestamp: string;
  body: string;
  query: string;
  params: string;
}

export const getReqMainInfo = (req): RequestInfo => {
  return {
    // HTTP方法 (GET, POST, PUT, DELETE等)
    method: req.method,

    // 请求的完整URL
    url: req.originalUrl || req.url,

    // 请求头信息
    headers: req.headers as Record<string, string>,

    // 获取请求的参数
    body: req.body,

    // get请求参数
    query: req.query,

    // params 请求参数
    params: req.params,

    // 获取客户端IP地址
    ip:
      req.ip ||
      req.connection.remoteAddress ||
      req.socket.remoteAddress ||
      (req.connection as any).socket?.remoteAddress ||
      "",

    // 用户代理信息
    userAgent: req.get("User-Agent"),

    // 请求时间戳
    timestamp: new Date().toISOString(),
  };
};