NestJS 驗(yàn)證

2023-09-08 17:44 更新

驗(yàn)證網(wǎng)絡(luò)應(yīng)用中傳遞的任何數(shù)據(jù)是一種最佳實(shí)踐。為了自動(dòng)驗(yàn)證傳入請(qǐng)求, Nest 提供了幾個(gè)開箱即用的管道。

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe

ValidationPipe 使用了功能強(qiáng)大的 class-validator 包及其聲明性驗(yàn)證裝飾器。 ValidationPipe 提供了一種對(duì)所有傳入的客戶端有效負(fù)載強(qiáng)制執(zhí)行驗(yàn)證規(guī)則的便捷方法,其中在每個(gè)模塊的本地類或者 DTO 聲明中使用簡單的注釋聲明特定的規(guī)則。

概覽

在 Pipes 一章中,我們完成了構(gòu)建簡化驗(yàn)證管道的過程。為了更好地了解我們?cè)谀缓笏龅墓ぷ?,我們?qiáng)烈建議您閱讀本文。在這里,我們將重點(diǎn)討論 ValidationPipe 的各種實(shí)際用例,并使用它的一些高級(jí)定制特性。

使用內(nèi)置的ValidationPipe

在開始使用之前,我們先安裝依賴。

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

ValidationPipe 從 @nestjs/common 包導(dǎo)入。

由于此管道使用了 class-validator 和 class-transformer 庫,因此有許多可用的選項(xiàng)。通過傳遞給管道的配置對(duì)象來進(jìn)行配置。依照下列內(nèi)置的選項(xiàng):

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

所有可用的class-validator選項(xiàng)(繼承自ValidatorOptions接口):

選項(xiàng)類型描述
enableDebugMessagesboolean如果設(shè)置為 true ,驗(yàn)證器會(huì)在出問題的時(shí)候打印額外的警告信息
skipUndefinedPropertiesboolean如果設(shè)置為 true ,驗(yàn)證器將跳過對(duì)所有驗(yàn)證對(duì)象中值為 null 的屬性的驗(yàn)證
skipNullPropertiesboolean如果設(shè)置為 true ,驗(yàn)證器將跳過對(duì)所有驗(yàn)證對(duì)象中值為 null 或 undefined 的屬性的驗(yàn)證
skipMissingPropertiesboolean如果設(shè)置為 true ,驗(yàn)證器將跳過對(duì)所有驗(yàn)證對(duì)象中缺失的屬性的驗(yàn)證
whitelistboolean如果設(shè)置為 true ,驗(yàn)證器將去掉沒有使用任何驗(yàn)證裝飾器的屬性的驗(yàn)證(返回的)對(duì)象
forbidNonWhitelistedboolean如果設(shè)置為 true ,驗(yàn)證器不會(huì)去掉非白名單的屬性,而是會(huì)拋出異常
forbidUnknownValuesboolean如果設(shè)置為 true ,嘗試驗(yàn)證未知對(duì)象會(huì)立即失敗
disableErrorMessageboolean如果設(shè)置為 true ,驗(yàn)證錯(cuò)誤不會(huì)返回給客戶端
errorHttpStatusCodenumber這個(gè)設(shè)置允許你確定在錯(cuò)誤時(shí)使用哪個(gè)異常類型。默認(rèn)拋出 BadRequestException
exceptionFactoryFunction接受一個(gè)驗(yàn)證錯(cuò)誤數(shù)組并返回一個(gè)要拋出的異常對(duì)象
groupsstring[]驗(yàn)證對(duì)象時(shí)使用的分組
alwaysboolean設(shè)置裝飾器選項(xiàng) always 的默認(rèn)值。默認(rèn)值可以在裝飾器的選項(xiàng)中被覆寫
strictGroupsboolean忽略在任何分組內(nèi)的裝飾器,如果 groups 沒有給出或者為空
dismissDefaultMessagesboolean如果設(shè)置為 true ,將不會(huì)使用默認(rèn)消息驗(yàn)證,如果不設(shè)置,錯(cuò)誤消息會(huì)始終是 undefined
validationError.targetboolean確定目標(biāo)是否要在 ValidationError 中暴露出來
validationError.valueboolean確定驗(yàn)證值是否要在 ValidationError 中暴露出來
stopAtFirstErrorboolean如果設(shè)置為 true ,對(duì)于給定的屬性的驗(yàn)證會(huì)在觸發(fā)第一個(gè)錯(cuò)誤之后停止。默認(rèn)為 false

更多關(guān)于class-validator包的內(nèi)容見項(xiàng)目倉庫。

自動(dòng)驗(yàn)證

為了本教程的目的,我們將綁定 ValidationPipe 到整個(gè)應(yīng)用程序,因此,將自動(dòng)保護(hù)所有接口免受不正確的數(shù)據(jù)的影響。

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

要測試我們的管道,讓我們創(chuàng)建一個(gè)基本接口。

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

由于 Typescript 沒有保存 泛型或接口 的元數(shù)據(jù)。當(dāng)你在你的 DTO 中使用他們的時(shí)候。 ValidationPipe 可能不能正確驗(yàn)證輸入數(shù)據(jù)。出于這種原因,可以考慮在你的 DTO 中使用具體的類。

當(dāng)你導(dǎo)入你的 DTO 時(shí),你不能使用僅類型的導(dǎo)入,因?yàn)轭愋蜁?huì)在運(yùn)行時(shí)被擦除,記得用 import { CreateUserDto } 而不是 import type { CreateUserDto } 。

現(xiàn)在我們可以在 CreateUserDto 中添加一些驗(yàn)證規(guī)則。我們使用 class-validator 包提供的裝飾器來實(shí)現(xiàn)這一點(diǎn),這里有詳細(xì)的描述。以這種方式,任何使用 CreateUserDto 的路由都將自動(dòng)執(zhí)行這些驗(yàn)證規(guī)則。

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

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

  @IsNotEmpty()
  password: string;
}

有了這些規(guī)則,當(dāng)某人使用無效 email 執(zhí)行對(duì)我們的接口的請(qǐng)求時(shí),則應(yīng)用程序?qū)⒆詣?dòng)以 400 Bad Request 代碼以及以下響應(yīng)正文進(jìn)行響應(yīng):

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

除了驗(yàn)證請(qǐng)求主體之外,ValidationPipe 還可以與其他請(qǐng)求對(duì)象屬性一起使用。假設(shè)我們希望接受端點(diǎn)路徑中的 id 。為了確保此請(qǐng)求參數(shù)只接受數(shù)字,我們可以使用以下結(jié)構(gòu):

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

與 DTO 一樣,F(xiàn)indOneParams 只是一個(gè)使用 class-validator 定義驗(yàn)證規(guī)則的類。它是這樣的:

import { IsNumberString } from 'class-validator';

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

禁用詳細(xì)錯(cuò)誤

錯(cuò)誤消息有助于解釋請(qǐng)求中的錯(cuò)誤。然而,一些生產(chǎn)環(huán)境傾向于禁用詳細(xì)的錯(cuò)誤。通過向 ValidationPipe 傳遞一個(gè)選項(xiàng)對(duì)象來做到這一點(diǎn):

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

現(xiàn)在,不會(huì)將錯(cuò)誤消息返回給最終用戶。

剝離屬性

我們的 ValidationPipe 還可以過濾掉方法處理程序不應(yīng)該接收的屬性。在這種情況下,我們可以對(duì)可接受的屬性進(jìn)行白名單,白名單中不包含的任何屬性都會(huì)自動(dòng)從結(jié)果對(duì)象中刪除。例如,如果我們的處理程序需要 email 和 password,但是一個(gè)請(qǐng)求還包含一個(gè) age 屬性,那么這個(gè)屬性可以從結(jié)果 DTO 中自動(dòng)刪除。要啟用這種行為,請(qǐng)將 whitelist 設(shè)置為 true 。

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

當(dāng)設(shè)置為 true 時(shí),這將自動(dòng)刪除非白名單屬性(在驗(yàn)證類中沒有任何修飾符的屬性)。

或者,您可以在出現(xiàn)非白名單屬性時(shí)停止處理請(qǐng)求,并向用戶返回錯(cuò)誤響應(yīng)。要啟用此選項(xiàng),請(qǐng)將 forbidNonWhitelisted 選項(xiàng)屬性設(shè)置為 true ,并將 whitelist 設(shè)置為 true。

負(fù)載對(duì)象轉(zhuǎn)換(Transform)

來自網(wǎng)絡(luò)的有效負(fù)載是普通的 JavaScript 對(duì)象。ValidationPipe 可以根據(jù)對(duì)象的 DTO 類自動(dòng)將有效負(fù)載轉(zhuǎn)換為對(duì)象類型。若要啟用自動(dòng)轉(zhuǎn)換,請(qǐng)將 transform 設(shè)置為 true。這可以在方法級(jí)別使用:

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

要在全局啟用這一行為,將選項(xiàng)設(shè)置到一個(gè)全局管道中:

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

要使能自動(dòng)轉(zhuǎn)換選項(xiàng),ValidationPipe將執(zhí)行簡單類型轉(zhuǎn)換。在下述示例中,findOne()方法調(diào)用一個(gè)從地址參數(shù)中解析出的id參數(shù)。

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

默認(rèn)地,每個(gè)地址參數(shù)和查詢參數(shù)在網(wǎng)絡(luò)傳輸時(shí)都是 string 類型。在上述示例中,我們指定 id 參數(shù)為 number (在方法簽名中)。因此,ValidationPipe會(huì)自動(dòng)將 string 類型轉(zhuǎn)換為 number 。

顯式轉(zhuǎn)換

在上述部分,我們演示了 ValidationPipe 如何基于期待類型隱式轉(zhuǎn)換查詢和路徑參數(shù),然而,這一特性需要開啟自動(dòng)轉(zhuǎn)換功能。

可選地(在不開啟自動(dòng)轉(zhuǎn)換功能的情況下),你可以使用 ParseIntPipe 或者 ParseBoolPipe 顯式處理值(注意,沒有必要使用 ParseStringPipe ,這是因?yàn)槿缜八龅?,網(wǎng)絡(luò)中傳輸?shù)穆窂絽?shù)和查詢參數(shù)默認(rèn)都是 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包中導(dǎo)出。

映射類型

當(dāng)你在編寫如增刪改查(新增/刪除/修改/查詢)的新功能的時(shí)候,你會(huì)經(jīng)?;谝粋€(gè)實(shí)體類型來構(gòu)造一個(gè)變種。 Nest 提供了一些可以進(jìn)行類型轉(zhuǎn)換的功能函數(shù)來讓這種任務(wù)更加方便。

如果你的應(yīng)用使用了 @nestjs/swagger 包,請(qǐng)看這一章節(jié)來了解更多有關(guān)映射類型的信息。類似地,如果你使用了 @nestjs/graphql 包請(qǐng)看這一章節(jié)。這幾個(gè)包都十分依賴類型所以需要分開導(dǎo)入以使用。因此,如果你使用了 @nestjs/mapped-types (而不是合適的包,根據(jù)你應(yīng)用的類型是 @nestjs/swagger 或者 @nestjs/graphql ),你可能會(huì)碰到各種各樣的沒有被文檔記錄的副作用。

當(dāng)構(gòu)造輸入驗(yàn)證類型(也稱為 DTO )時(shí),你往往會(huì)在同一個(gè)類型上構(gòu)造 創(chuàng)建 和 更新 變種。舉個(gè)例子, 創(chuàng)建 變種可能要求全部的字段都被填寫,但是 更新 變種可能會(huì)把全部的字段變成可選的。

Nest 提供了 PartialType() 函數(shù)來讓這個(gè)任務(wù)變得簡單,同時(shí)也可以減少樣板代碼。

PartialType() 函數(shù)返回一個(gè)類型(一個(gè)類)包含被設(shè)置成可選的所有輸入類型的屬性。假設(shè)我們有一個(gè) 創(chuàng)建 的類型:

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

在默認(rèn)情況下,所有的字段都是被需要的。使用 PartialType() 并把類引用( CreateCatDto )當(dāng)作參數(shù)傳入就可以創(chuàng)造一個(gè)有著相同字段但是每一個(gè)字段都是可選的新類型:

export class UpdateCatDto extends PartialType(CreateCatDto) {}

PartialType() 函數(shù)是從 @nestjs/mapped-types 包導(dǎo)入的。

PickType() 函數(shù)通過挑出輸入類型的一組屬性構(gòu)造一個(gè)新的類型(類)。假設(shè)我們有以下的類型:

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

我們可以使用 PickType() 函數(shù)從這個(gè)類中挑出一組屬性:

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

PickType() 函數(shù)是從 @nestjs/mapped-types 包導(dǎo)入的。

OmitType() 函數(shù)通過挑出輸入類型中的全部屬性,然后移除一組特定的屬性構(gòu)造一個(gè)類型。假設(shè)我們有以下的類型:

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

如下所示,我們可以生成一個(gè)派生的擁有除了 name 以外的所有屬性的類型。在這個(gè)結(jié)構(gòu)中,給 OmitType() 的第二個(gè)參數(shù)是一個(gè)包含了屬性名的數(shù)組:

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

OmitType() 函數(shù)是從 @nestjs/mapped-types 包導(dǎo)入的。

IntersectionType() 函數(shù)將兩個(gè)類型合并成一個(gè)類型。假設(shè)我們有以下的兩個(gè)類型:

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

export class AdditionalCatInfo {
  color: string;
}

我們可以生成一個(gè)合并了兩個(gè)類型中所有屬性的新類型:

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

IntersectionType() 函數(shù)是從 @nestjs/mapped-types 包導(dǎo)入的。

這些映射類型函數(shù)是可以組合的。下面的例子會(huì)創(chuàng)造一個(gè)擁有除了 name 屬性以外所有的 CreateCatDto 的屬性,而且這些屬性是可選的:

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

轉(zhuǎn)換和驗(yàn)證數(shù)組

TypeScript 不存儲(chǔ)泛型或接口的元數(shù)據(jù),因此當(dāng)你在 DTO 中使用它們的時(shí)候, ValidationPipe 可能不能正確驗(yàn)證輸入數(shù)據(jù)。例如,在下列代碼中, createUserDto 不能正確驗(yàn)證。

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

要驗(yàn)證數(shù)組,創(chuàng)建一個(gè)包裹了該數(shù)組的專用類,或者使用 ParseArrayPipe 。

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

此外, ParseArrayPipe 可能需要手動(dòng)解析查詢參數(shù)。讓我們考慮一個(gè)返回作為查詢參數(shù)傳遞的標(biāo)識(shí)的 users 的 findByIds() 方法:

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

這個(gè)構(gòu)造用于驗(yàn)證一個(gè)來自如下形式帶參數(shù)的 GET 請(qǐng)求:

GET /?ids=1,2,3

Websockets 和 微服務(wù)

盡管本章展示了使用 HTTP 風(fēng)格的應(yīng)用程序的例子(例如,Express或 Fastify ), ValidationPipe 對(duì)于 WebSockets 和微服務(wù)是一樣的,不管使用什么傳輸方法。

學(xué)到更多

要閱讀有關(guān)由 class-validator 提供的自定義驗(yàn)證器,錯(cuò)誤消息和可用裝飾器的更多信息,請(qǐng)?jiān)L問此頁面。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)