NestJS 学习笔记 3 (管道、守卫、拦截器)

300 阅读4分钟

接着上一篇记录一下学习笔记

管道

管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。

  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;

管道转换

NestJS 有一些内置管道可用

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe

用法:

 @Get()
 create(
    @Query('id', ParseIntPipe) id: number,  // string 转 int
  ) {
  }

自定义转换

user-by-id.pipe.ts

@Injectable()
export class UserByIdPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): UserEntity {
    const id = parseInt(value);
    // 转换需要的数据
    const userEntity: UserEntity = await this.userRepo.find(id)
    return userEntity;
  }
}

控制器

@Get(':id') findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
    return userEntity; 
}

管道验证 DTO

安装

npm i --save class-validator class-transformer

写 DTO 文件

add-project.dto.ts

import { IsArray, IsNotEmpty, IsNumber, IsString } from "class-validator"

export class AddProjectDto {
    // 客户表的 id
    @IsNotEmpty()     
    @IsNumber()       // 注意不要写小写 isNumber()
    customerId: number
    // 应用表的 id	
    @IsNotEmpty()
    @IsNumber()
    applicationId: number
    // 专案名称,同一个客户下名字不能重复	
    @IsNotEmpty()
    @IsString()
    projectName: string
    // 项目描述	
    @IsNotEmpty()
    @IsString()
    desc: string
    // 元件,数组
    @IsArray()
    compnentIds: number[]
}

控制器

  // 新增专案
  @Post()
  async addProject(@Body() addProjectDto: AddProjectDto) {
    return this.trackingService.addProject(addProjectDto)
  }

基本上这样就可以了, 如果有错, 会抛出错误信息. class-validator 写法, 可以看官网

全局验证

main.js

  // 全局注册通用验证管道 ValidationPipe
  app.useGlobalPipes(new ValidationPipe());

validation.pipe.ts

import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform<any>{
    // value 是当前处理的参数, metatype 是属性的元类型, data 是当前处理的参数名,  type 是 'body' | 'query' | 'param' | 'custom'
    async transform(value: any, { metatype, data, type }: ArgumentMetadata) {
        // 全局验证进去这里, 可以对数据进行验证.
        // 这里验证 tab_status, 是不是数组 ['Alive', 'PilotRun', 'MP', 'P/L', 'Drafts'] 之中之一, 如果不是, 抛出异常.
        if (data === 'tab_status') {
            const arr = ['Alive', 'PilotRun', 'MP', 'P/L', 'Drafts']
            const result = arr.indexOf(value)
            if (result < -1) {
                throw new HttpException(data + " 必须包含 'Alive', 'PilotRun', 'MP', 'P/L', 'Drafts' 之一", HttpStatus.BAD_REQUEST)
            }
        }
        if (!metatype || !this.toValidate(metatype)) {
            return value;
        }
        // plainToclass 方法将普通的javascript对象转换为特定类的实例
        const object = plainToClass(metatype, value);
        // 验证该对象返回出错的数组
        const errors = await validate(object);
        if (errors.length > 0) {
            // 将错误信息数组中的第一个内容返回给异常过滤器
            let errormsg = errors.shift().constraints;
            throw new BadRequestException(errormsg);
        }
        return value;
    }

    // 验证属性值的元类型是否是String, Boolean, Number, Array, Object 中的一种
    private toValidate(metatype: any): boolean {
        const types: Function[] = [String, Boolean, Number, Array, Object];
        return !types.includes(metatype);
    }
}

当然, 上面的全局验证数据, 也可以做局部验证.

tab_status.validation.ts

import { ArgumentMetadata, HttpException, HttpStatus, Logger, PipeTransform } from "@nestjs/common";

export class TabStatusValidation implements PipeTransform {
    // value 是当前处理的参数, metatype 是属性的元类型, data 是当前处理的参数名,  type 是 'body' | 'query' | 'param' | 'custom'
    transform(value: any, { metatype, data, type }: ArgumentMetadata) {
        if (data === 'tab_status') {
            const arr = ['Alive', 'PilotRun', 'MP', 'P/L', 'Drafts']
            const result = arr.indexOf(value)
            if (result < -1) {
                throw new HttpException(data + " 必须包含 'Alive', 'PilotRun', 'MP', 'P/L', 'Drafts' 之一", HttpStatus.BAD_REQUEST)
            }
        }
        return value
    }

}

控制器

// TabStatusValidation 局部验证 tab_status
  @Get('list')
  getList(@Query('tab_status', TabStatusValidation) tabStatus: string, @Query('sort') sort: string) {
    return this.trackingService.getList({ tabStatus, sort });
  }

守卫

守卫有一个单独的责任。它们根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。 这通常称为授权。在传统的 Express 应用程序中,通常由中间件处理授权。中间件是身份验证的良好选择。到目前为止,访问限制逻辑大多在中间件内。这样很好,因为诸如 token 验证或将 request 对象附加属性与特定路由没有强关联。

中间件不知道调用 next() 函数后会执行哪个处理程序。另一方面,守卫可以访问 ExecutionContext 实例,因此确切地知道接下来要执行什么。它们的设计与异常过滤器、管道和拦截器非常相似,目的是让您在请求/响应周期的正确位置插入处理逻辑,并以声明的方式进行插入。这有助于保持代码的简洁和声明性。

守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。

token

auth.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable() 
export class AuthGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        const request: Request = context.switchToHttp().getRequest();
        // 白名单验证
        if (this.hasUrl(this.urlList, request.url)) {
            return true;
        }
        // 取得 token
        const token = context.switchToRpc().getData().headers.authorization;
        
        if (token) {
            const isTure = token // token 验证, 代码实际处理, token 可以用 jwtService
            // ...
            if (isTure) {
                return true
            } else {
                throw new HttpException("token 已经失效", HttpStatus.UNAUTHORIZED)
            }
        } else {
            throw new HttpException("请请先登录", HttpStatus.UNAUTHORIZED)
        }
    }
    
    // 白名单
    private urlList: string[] = [
        '/api/v1/user/login',
        '/api/v1/user/pwd',
        '/api/v1/user/forget',
        '/api/v1/user/verifycode',
    ];
    
    // 验证请求是否为白名单的路由
    private hasUrl(urlList: string[], url: string): boolean {
        let flag: boolean = false;
        if (urlList.indexOf(url.split('?')[0]) >= 0) {
            flag = true;
        }
        return flag;
    }
}

全局注册

main.ts

  // 全局注册权限验证守卫
  app.useGlobalGuards(new AuthGuard());

路由角色认证

利用装修器验证 admin 路由等..

roles.decorator.ts

import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

控制器

cats.controller.ts

@Post()
@Roles('admin')   // 验证有没有 admin 权限, 这个权限可以放在 token 里加密在解密, 也可以后面自己查找验证
async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto); 
}

路由守卫

roles.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) { }

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    // 取得 token
        const token = context.switchToRpc().getData().headers.authorization;
    // 这里可以把用户权限放 token 里面. 在这里可以解密取得 user 用户信息
    先对 token 验证
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

更进一步写法, 以下是都是伪代码, 意思传达就好.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) { }

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    // 取得 token
        const token = context.switchToRpc().getData().headers.authorization;
    // 这里可以把用户权限放 token 里面. 在这里可以解密取得 user 用户信息
    先对 token 验证
    if(token) {
        // 验证通过 
        return true;
    } else {
        // 抛异常
    }
    if (!roles) {
      return true;
    }
    // 这里是 token 解密后取得 user
    const user = token.user;
    // 对比是不是有权限
    return matchRoles(roles, user.roles);
  }
}

拦截器

拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:

  • 在函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 扩展基本函数行为
  • 根据所选条件完全重写函数 (例如, 缓存目的)

Response 拦截后, 统一返回数据格式

{
    statusCode: 0,
    message: '成功',
    data: data
}

response.interceptor.ts

import { Injectable, NestInterceptor, CallHandler } from '@nestjs/common'
import { map } from 'rxjs/operators'
import {Observable} from 'rxjs'
 
 
 
interface data<T>{
    data:T
}
 
@Injectable()
export class Response<T = any> implements NestInterceptor {
    intercept(context, next: CallHandler):Observable<data<T>> {
        return next.handle().pipe(map(data => {
            return {
               statusCode: 0,
               message: '成功',
               data: data
            }
        }))
    }
}

全局注册

main.ts

// 全局注册拦截器
app.useGlobalInterceptors(new ResponseInterceptor())

当然, 也可以局部注册

cats.controller.ts

@UseInterceptors(ResponseInterceptor)
export class CatsController {}

以上就是简单的记录一下, 详情可以看官方文档