验证 Validation

导读

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

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe

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

概述

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

使用内置 ValidationPipe

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

bash
$ npm i --save class-validator class-transformer
提示

ValidationPipe@nestjs/common 包导出。

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

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

除此之外,所有 class-validator 选项(继承自 ValidatorOptions 接口)都可用:

OptionTypeDescription
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,从而确保所有端点都受到保护,不会接收不正确的数据。

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

为了测试我们的管道,让我们创建一个基本的端点。

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

由于 TypeScript 不存储有关 泛型或接口 的元数据,因此当您在 DTO 中使用它们时,ValidationPipe 可能无法正确验证传入的数据。因此,请考虑在 DTO 中使用具体类。

Hint

导入 DTO 时,您不能使用仅类型导入,因为这会在运行时被删除,即记住 import {{ '{' }} CreateUserDto {{ '}' }} 而不是 import type {{ '{' }} CreateUserDto {{ '}' }}

现在我们可以在 CreateUserDto 中添加一些验证规则。我们使用 class-validator 包提供的装饰器来执行此操作,此处 有详细描述。这样,任何使用 CreateUserDto 的路由都会自动执行这些验证规则。

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

export class CreateUserDto {
  @IsEmail()
email: string

  @IsNotEmpty()
password: string
}

有了这些规则,如果请求在请求正文中带有无效的 email 属性时到达我们的端点,应用程序将自动响应 400 Bad Request 代码以及以下响应正文:

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

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

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

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

ts
import { IsNumberString } from 'class-validator'

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

禁用详细错误

错误消息有助于解释请求中的错误。但是,某些生产环境更喜欢禁用详细错误。通过将选项对象传递给 ValidationPipe 来实现此目的:

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

因此,详细的错误消息不会显示在响应主体中。

剥离属性

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

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

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

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

转换有效负载对象

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

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

要全局启用此行为,请在全局管道上设置选项:

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

启用自动转换选项后,ValidationPipe 还将执行原始类型的转换。在以下示例中,findOne() 方法接受一个参数,该参数表示提取的 id 路径参数:

ts
@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 的形式通过网络传输)。

ts
@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)时,在同一类型上构建 createupdate 变体通常很有用。例如,create 变体可能需要所有字段,而 update 变体可能使所有字段都可选。

Nest 提供了 PartialType() 实用函数来简化此任务并最大限度地减少样板。

PartialType() 函数返回一个类型(类),其中输入类型的所有属性都设置为可选。例如,假设我们有一个 create 类型,如下所示:

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

默认情况下,所有这些字段都是必需的。要创建具有相同字段但每个字段都是可选的类型,请使用 PartialType() 传递类引用 (CreateCatDto) 作为参数:

ts
export class UpdateCatDto extends PartialType(CreateCatDto) {}
Hint

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

PickType() 函数通过从输入类型中选择一组属性来构造新类型(类)。例如,假设我们从以下类型开始:

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

我们可以使用 PickType() 实用函数从此类中挑选一组属性:

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

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

OmitType() 函数通过从输入类型中挑选所有属性,然后删除一组特定的键来构造类型。例如,假设我们从如下类型开始:

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

我们可以生成一个派生类型,该类型具有除 name 之外的所有属性,如下所示。在此构造中,OmitType 的第二个参数是属性名称数组。

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

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

IntersectionType() 函数将两种类型组合成一种新类型(类)。例如,假设我们从两种类型开始:

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

export class AddedCatInfo {
  color: string
}

我们可以生成一种新类型,将两种类型的所有属性组合在一起。

ts
export class UpdateCatDto extends IntersectionType(
  CreateCatDto,
  AddedCatInfo,
) {}
提示

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

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

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

解析和验证数组

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

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

要验证数组,请创建一个包含包装数组的属性的专用类,或使用 ParseArrayPipe

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

此外,ParseArrayPipe 在解析查询参数时可能会派上用场。让我们考虑一个 findByIds() 方法,该方法根据作为查询参数传递的标识符返回用户。

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

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

bash
GET /?ids=1,2,3

WebSockets 和微服务

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

了解更多

阅读有关 class-validator 包提供的自定义验证器、错误消息和可用装饰器的更多信息此处