Skip to content

验证

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

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe

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

概述

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

使用内置的 ValidationPipe

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

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

提示

ValidationPipe@nestjs/common 包导出。

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

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

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

选项类型描述
enableDebugMessagesboolean如果设置为 true,验证器将在出现问题时向控制台打印额外的警告消息。
skipUndefinedPropertiesboolean如果设置为 true,验证器将跳过验证对象中所有 undefined 属性的验证。
skipNullPropertiesboolean如果设置为 true,验证器将跳过验证对象中所有 null 属性的验证。
skipMissingPropertiesboolean如果设置为 true,验证器将跳过验证对象中所有 null 或 undefined 属性的验证。
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,从而确保所有端点都受到保护,不会接收不正确的数据。

typescript
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

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

typescript
@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 的路由都将自动执行这些验证规则。

typescript
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。为确保此请求参数只接受数字,我们可以使用以下构造:

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

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

typescript
import { IsNumberString } from 'class-validator';

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

禁用详细错误

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

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

这样,详细错误消息将不会显示在响应体中。

去除属性

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

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

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

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

转换负载对象

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

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

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

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

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

typescript
@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 通过网络传入)。

typescript
@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 类型:

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

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

typescript
export class UpdateCatDto extends PartialType(CreateCatDto) {}

提示

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

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

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

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

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

提示

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

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

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

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

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

提示

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

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

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

export class AdditionalCatInfo {
  color: string;
}

我们可以生成一个组合两种类型中所有属性的新类型。

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

提示

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

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

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

解析和验证数组

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

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

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

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

此外,ParseArrayPipe 在解析查询参数时也很方便。让我们考虑一个 findByIds() 方法,它根据作为查询参数传递的标识符返回用户。

typescript
@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 包提供的自定义验证器、错误消息和可用装饰器。

基于 NestJS 官方文档翻译