跳到主要内容

校验

验证任何发送到 Web 应用程序的数据的正确性是最佳实践。 为了自动验证传入的请求,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-validatorclass-transformer 库,所以有很多可用的选项。 您可以通过传递给管道的配置对象来配置这些设置。 以下是内置选项:

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

除了这些选项外,还可以使用从 ValidatorOptions 接口继承的所有 class-validator 选项:

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如果未给出 groupsgroups 为空,则忽略至少有一组的装饰器
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(3000);
}
bootstrap();

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

@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: number;
}

禁用详细错误

错误消息有助于解释请求中的错误内容。但是,某些生产环境更喜欢禁用详细错误。 通过将选项对象传递给 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。这可以在方法级别完成:

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

显式转换

在上一节中,我们展示了 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 不存储泛型或接口的元数据。 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';
}

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

GET /?ids=1,2,3

WebSockets 和微服务

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

了解更多信息

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