NestJS 验证

2023-09-08 17:44 更新

验证网络应用中传递的任何数据是一种最佳实践。为了自动验证传入请求, Nest 提供了几个开箱即用的管道。

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe

ValidationPipe 使用了功能强大的 class-validator 包及其声明性验证装饰器。 ValidationPipe 提供了一种对所有传入的客户端有效负载强制执行验证规则的便捷方法,其中在每个模块的本地类或者 DTO 声明中使用简单的注释声明特定的规则。

概览

在 Pipes 一章中,我们完成了构建简化验证管道的过程。为了更好地了解我们在幕后所做的工作,我们强烈建议您阅读本文。在这里,我们将重点讨论 ValidationPipe 的各种实际用例,并使用它的一些高级定制特性。

使用内置的ValidationPipe

在开始使用之前,我们先安装依赖。

$ npm i --save class-validator class-transformer

ValidationPipe 从 @nestjs/common 包导入。

由于此管道使用了 class-validator 和 class-transformer 库,因此有许多可用的选项。通过传递给管道的配置对象来进行配置。依照下列内置的选项:

export interface ValidationPipeOptions extends ValidatorOptions {
  transform?: boolean;
  disableErrorMessages?: boolean;
  exceptionFactory?: (errors: ValidationError[]) => any;
}

所有可用的class-validator选项(继承自ValidatorOptions接口):

选项类型描述
enableDebugMessagesboolean如果设置为 true ,验证器会在出问题的时候打印额外的警告信息
skipUndefinedPropertiesboolean如果设置为 true ,验证器将跳过对所有验证对象中值为 null 的属性的验证
skipNullPropertiesboolean如果设置为 true ,验证器将跳过对所有验证对象中值为 null 或 undefined 的属性的验证
skipMissingPropertiesboolean如果设置为 true ,验证器将跳过对所有验证对象中缺失的属性的验证
whitelistboolean如果设置为 true ,验证器将去掉没有使用任何验证装饰器的属性的验证(返回的)对象
forbidNonWhitelistedboolean如果设置为 true ,验证器不会去掉非白名单的属性,而是会抛出异常
forbidUnknownValuesboolean如果设置为 true ,尝试验证未知对象会立即失败
disableErrorMessageboolean如果设置为 true ,验证错误不会返回给客户端
errorHttpStatusCodenumber这个设置允许你确定在错误时使用哪个异常类型。默认抛出 BadRequestException
exceptionFactoryFunction接受一个验证错误数组并返回一个要抛出的异常对象
groupsstring[]验证对象时使用的分组
alwaysboolean设置装饰器选项 always 的默认值。默认值可以在装饰器的选项中被覆写
strictGroupsboolean忽略在任何分组内的装饰器,如果 groups 没有给出或者为空
dismissDefaultMessagesboolean如果设置为 true ,将不会使用默认消息验证,如果不设置,错误消息会始终是 undefined
validationError.targetboolean确定目标是否要在 ValidationError 中暴露出来
validationError.valueboolean确定验证值是否要在 ValidationError 中暴露出来
stopAtFirstErrorboolean如果设置为 true ,对于给定的属性的验证会在触发第一个错误之后停止。默认为 false

更多关于class-validator包的内容见项目仓库

自动验证

为了本教程的目的,我们将绑定 ValidationPipe 到整个应用程序,因此,将自动保护所有接口免受不正确的数据的影响。

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

要测试我们的管道,让我们创建一个基本接口。

@Post()
create(@Body() createUserDto: CreateUserDto) {
  return 'This action adds a new user';
}

由于 Typescript 没有保存 泛型或接口 的元数据。当你在你的 DTO 中使用他们的时候。 ValidationPipe 可能不能正确验证输入数据。出于这种原因,可以考虑在你的 DTO 中使用具体的类。

当你导入你的 DTO 时,你不能使用仅类型的导入,因为类型会在运行时被擦除,记得用 import { CreateUserDto } 而不是 import type { CreateUserDto } 。

现在我们可以在 CreateUserDto 中添加一些验证规则。我们使用 class-validator 包提供的装饰器来实现这一点,这里有详细的描述。以这种方式,任何使用 CreateUserDto 的路由都将自动执行这些验证规则。

import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsNotEmpty()
  password: string;
}

有了这些规则,当某人使用无效 email 执行对我们的接口的请求时,则应用程序将自动以 400 Bad Request 代码以及以下响应正文进行响应:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": ["email must be an email"]
}

除了验证请求主体之外,ValidationPipe 还可以与其他请求对象属性一起使用。假设我们希望接受端点路径中的 id 。为了确保此请求参数只接受数字,我们可以使用以下结构:

@Get(':id')
findOne(@Param() params: FindOneParams) {
  return 'This action returns a user';
}

与 DTO 一样,FindOneParams 只是一个使用 class-validator 定义验证规则的类。它是这样的:

import { IsNumberString } from 'class-validator';

export class FindOneParams {
  @IsNumberString()
  id: number;
}

禁用详细错误

错误消息有助于解释请求中的错误。然而,一些生产环境倾向于禁用详细的错误。通过向 ValidationPipe 传递一个选项对象来做到这一点:

app.useGlobalPipes(
  new ValidationPipe({
    disableErrorMessages: true,
  })
);

现在,不会将错误消息返回给最终用户。

剥离属性

我们的 ValidationPipe 还可以过滤掉方法处理程序不应该接收的属性。在这种情况下,我们可以对可接受的属性进行白名单,白名单中不包含的任何属性都会自动从结果对象中删除。例如,如果我们的处理程序需要 email 和 password,但是一个请求还包含一个 age 属性,那么这个属性可以从结果 DTO 中自动删除。要启用这种行为,请将 whitelist 设置为 true 。

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
  })
);

当设置为 true 时,这将自动删除非白名单属性(在验证类中没有任何修饰符的属性)。

或者,您可以在出现非白名单属性时停止处理请求,并向用户返回错误响应。要启用此选项,请将 forbidNonWhitelisted 选项属性设置为 true ,并将 whitelist 设置为 true。

负载对象转换(Transform)

来自网络的有效负载是普通的 JavaScript 对象。ValidationPipe 可以根据对象的 DTO 类自动将有效负载转换为对象类型。若要启用自动转换,请将 transform 设置为 true。这可以在方法级别使用:

cats.control.ts
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

要在全局启用这一行为,将选项设置到一个全局管道中:

app.useGlobalPipes(
  new ValidationPipe({
    transform: true,
  })
);

要使能自动转换选项,ValidationPipe将执行简单类型转换。在下述示例中,findOne()方法调用一个从地址参数中解析出的id参数。

@Get(':id')
findOne(@Param('id') id: number) {
  console.log(typeof id === 'number'); // true
  return 'This action returns a user';
}

默认地,每个地址参数和查询参数在网络传输时都是 string 类型。在上述示例中,我们指定 id 参数为 number (在方法签名中)。因此,ValidationPipe会自动将 string 类型转换为 number 。

显式转换

在上述部分,我们演示了 ValidationPipe 如何基于期待类型隐式转换查询和路径参数,然而,这一特性需要开启自动转换功能。

可选地(在不开启自动转换功能的情况下),你可以使用 ParseIntPipe 或者 ParseBoolPipe 显式处理值(注意,没有必要使用 ParseStringPipe ,这是因为如前所述的,网络中传输的路径参数和查询参数默认都是 string 类型)。

@Get(':id')
findOne(
  @Param('id', ParseIntPipe) id: number,
  @Query('sort', ParseBoolPipe) sort: boolean,
) {
  console.log(typeof id === 'number'); // true
  console.log(typeof sort === 'boolean'); // true
  return 'This action returns a user';
}

ParseIntPipe和ParseBoolPipe从@nestjs/common包中导出。

映射类型

当你在编写如增删改查(新增/删除/修改/查询)的新功能的时候,你会经常基于一个实体类型来构造一个变种。 Nest 提供了一些可以进行类型转换的功能函数来让这种任务更加方便。

如果你的应用使用了 @nestjs/swagger 包,请看这一章节来了解更多有关映射类型的信息。类似地,如果你使用了 @nestjs/graphql 包请看这一章节。这几个包都十分依赖类型所以需要分开导入以使用。因此,如果你使用了 @nestjs/mapped-types (而不是合适的包,根据你应用的类型是 @nestjs/swagger 或者 @nestjs/graphql ),你可能会碰到各种各样的没有被文档记录的副作用。

当构造输入验证类型(也称为 DTO )时,你往往会在同一个类型上构造 创建 和 更新 变种。举个例子, 创建 变种可能要求全部的字段都被填写,但是 更新 变种可能会把全部的字段变成可选的。

Nest 提供了 PartialType() 函数来让这个任务变得简单,同时也可以减少样板代码。

PartialType() 函数返回一个类型(一个类)包含被设置成可选的所有输入类型的属性。假设我们有一个 创建 的类型:

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

在默认情况下,所有的字段都是被需要的。使用 PartialType() 并把类引用( CreateCatDto )当作参数传入就可以创造一个有着相同字段但是每一个字段都是可选的新类型:

export class UpdateCatDto extends PartialType(CreateCatDto) {}

PartialType() 函数是从 @nestjs/mapped-types 包导入的。

PickType() 函数通过挑出输入类型的一组属性构造一个新的类型(类)。假设我们有以下的类型:

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

我们可以使用 PickType() 函数从这个类中挑出一组属性:

export class UpdateCatAgeDto extends PickType(CreateCatDto, ['age'] as const) {}

PickType() 函数是从 @nestjs/mapped-types 包导入的。

OmitType() 函数通过挑出输入类型中的全部属性,然后移除一组特定的属性构造一个类型。假设我们有以下的类型:

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

如下所示,我们可以生成一个派生的拥有除了 name 以外的所有属性的类型。在这个结构中,给 OmitType() 的第二个参数是一个包含了属性名的数组:

export class UpdateCatDto extends OmitType(CreateCatDto, ['name'] as const) {}

OmitType() 函数是从 @nestjs/mapped-types 包导入的。

IntersectionType() 函数将两个类型合并成一个类型。假设我们有以下的两个类型:

export class CreateCatDto {
  name: string;
  breed: string;
}

export class AdditionalCatInfo {
  color: string;
}

我们可以生成一个合并了两个类型中所有属性的新类型:

export class UpdateCatDto extends IntersectionType(
  CreateCatDto,
  AdditionalCatInfo,
) {}

IntersectionType() 函数是从 @nestjs/mapped-types 包导入的。

这些映射类型函数是可以组合的。下面的例子会创造一个拥有除了 name 属性以外所有的 CreateCatDto 的属性,而且这些属性是可选的:

export class UpdateCatDto extends PartialType(
  OmitType(CreateCatDto, ['name'] as const),
) {}

转换和验证数组

TypeScript 不存储泛型或接口的元数据,因此当你在 DTO 中使用它们的时候, ValidationPipe 可能不能正确验证输入数据。例如,在下列代码中, createUserDto 不能正确验证。

@Post()
createBulk(@Body() createUserDtos: CreateUserDto[]) {
  return 'This action adds new users';
}

要验证数组,创建一个包裹了该数组的专用类,或者使用 ParseArrayPipe 。

@Post()
createBulk(
  @Body(new ParseArrayPipe({ items: CreateUserDto }))
  createUserDtos: CreateUserDto[],
) {
  return 'This action adds new users';
}

此外, ParseArrayPipe 可能需要手动解析查询参数。让我们考虑一个返回作为查询参数传递的标识的 users 的 findByIds() 方法:

@Get()
findByIds(
  @Query('id', new ParseArrayPipe({ items: Number, separator: ',' }))
  ids: number[],
) {
  return 'This action returns users by ids';
}

这个构造用于验证一个来自如下形式带参数的 GET 请求:

GET /?ids=1,2,3

Websockets 和 微服务

尽管本章展示了使用 HTTP 风格的应用程序的例子(例如,Express或 Fastify ), ValidationPipe 对于 WebSockets 和微服务是一样的,不管使用什么传输方法。

学到更多

要阅读有关由 class-validator 提供的自定义验证器,错误消息和可用装饰器的更多信息,请访问此页面


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号