Techniques

验证

学习如何在 NestJS 中使用 ValidationPipe 和 class-validator 来验证传入的请求数据,包括自动验证、转换和映射类型。

验证

验证发送到 Web 应用程序的任何数据的正确性是最佳实践。为了自动验证传入的请求,Nest 提供了几个开箱即用的管道:

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe

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

概述

管道章节中,我们经历了构建简单管道并将它们绑定到控制器、方法或全局应用程序的过程,以演示该过程的工作原理。请务必查看该章节以最好地理解本章的主题。在这里,我们将专注于 ValidationPipe 的各种真实世界用例,并展示如何使用其一些高级自定义功能。

使用内置的 ValidationPipe

要开始使用它,我们首先安装所需的依赖项。

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

提示 ValidationPipe@nestjs/common 包中导出。

因为此管道使用 class-validatorclass-transformer 库,所以有许多可用选项。您可以通过传递给管道的配置对象来配置这些设置。以下是内置选项:

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

除了这些之外,所有 class-validator 选项(从 ValidatorOptions 接口继承)都可用:

选项类型描述
enableDebugMessagesboolean如果设置为 true,验证器将在控制台打印额外的警告消息,当某些内容不正确时。
skipUndefinedPropertiesboolean如果设置为 true,则验证器将跳过验证对象中所有未定义的属性。
skipNullPropertiesboolean如果设置为 true,则验证器将跳过验证对象中所有为 null 的属性。
skipMissingPropertiesboolean如果设置为 true,则验证器将跳过验证对象中所有为 null 或未定义的属性。
whitelistboolean如果设置为 true,验证器将从验证(返回)对象中剥离任何不使用任何验证装饰器的属性。
forbidNonWhitelistedboolean如果设置为 true,验证器将抛出异常,而不是剥离非白名单属性。
forbidUnknownValuesboolean如果设置为 true,尝试验证未知对象将立即失败。
disableErrorMessagesboolean如果设置为 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(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(process.env.PORT ?? 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';
}

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

import { IsNumberString } from 'class-validator';

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

禁用详细错误

错误消息有助于解释请求中的错误之处。但是,某些生产环境更喜欢禁用详细错误。通过向 ValidationPipe 传递选项对象来做到这一点:

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

结果,详细的错误消息不会显示在响应体中。

剥离属性

我们的 ValidationPipe 还可以过滤掉方法处理程序不应接收的属性。在这种情况下,我们可以白名单可接受的属性,任何不包含在白名单中的属性都会自动从结果对象中剥离。例如,如果我们的处理程序期望 emailpassword 属性,但请求还包括 age 属性,则此属性可以自动从结果 DTO 中删除。要启用此行为,请将 whitelist 设置为 true

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

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

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

转换负载对象

通过网络传入的负载是普通的 JavaScript 对象。ValidationPipe 可以自动将负载转换为根据其 DTO 类类型化的对象。要启用自动转换,请将 transform 设置为 true。这可以在方法级别完成:

@@filename(cats.controller)
@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 将尝试自动将字符串标识符转换为数字。

显式转换

在上面的部分中,我们展示了 ValidationPipe 如何根据预期类型隐式转换查询和路径参数。但是,此功能需要启用自动转换。

或者(在禁用自动转换的情况下),您可以使用 ParseIntPipeParseBoolPipe 显式转换值(请注意,不需要 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';
}

提示 ParseIntPipeParseBoolPipe@nestjs/common 包中导出。

映射类型

当您构建CRUD(创建/读取/更新/删除)等功能时,在基本实体类型上构建变体通常很有用。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 包中导入。

类型映射实用函数是可组合的。例如,以下将产生一个类型(类),该类型具有 CreateCatDto 类型的所有属性,除了 name,并且这些属性将设置为可选:

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

解析和验证数组

TypeScript 不存储关于泛型或接口的元数据,因此当您在 DTO 中使用它们时,ValidationPipe 可能无法正确验证传入的数据。例如,在以下代码中,createUserDtos 不会被正确验证:

@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 在解析查询参数时可能会派上用场。让我们考虑一个 findByIds() 方法,该方法根据作为查询参数传递的标识符返回用户。

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

此构造验证来自 HTTP GET 请求的传入查询参数,如下所示:

GET /?ids=1,2,3

WebSockets 和微服务

虽然本章显示了使用 HTTP 样式应用程序(例如,Express 或 Fastify)的示例,但 ValidationPipe 对 WebSockets 和微服务的工作方式相同,无论使用何种传输方法。

了解更多

阅读有关 class-validator 包提供的自定义验证器、错误消息和可用装饰器的更多信息,请访问这里