驗(yàn)證網(wǎng)絡(luò)應(yīng)用中傳遞的任何數(shù)據(jù)是一種最佳實(shí)踐。為了自動(dòng)驗(yàn)證傳入請(qǐng)求, Nest 提供了幾個(gè)開箱即用的管道。
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í)定制特性。
在開始使用之前,我們先安裝依賴。
$ 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) | 類型 | 描述 |
---|---|---|
enableDebugMessages | boolean | 如果設(shè)置為 true ,驗(yàn)證器會(huì)在出問題的時(shí)候打印額外的警告信息 |
skipUndefinedProperties | boolean | 如果設(shè)置為 true ,驗(yàn)證器將跳過對(duì)所有驗(yàn)證對(duì)象中值為 null 的屬性的驗(yàn)證 |
skipNullProperties | boolean | 如果設(shè)置為 true ,驗(yàn)證器將跳過對(duì)所有驗(yàn)證對(duì)象中值為 null 或 undefined 的屬性的驗(yàn)證 |
skipMissingProperties | boolean | 如果設(shè)置為 true ,驗(yàn)證器將跳過對(duì)所有驗(yàn)證對(duì)象中缺失的屬性的驗(yàn)證 |
whitelist | boolean | 如果設(shè)置為 true ,驗(yàn)證器將去掉沒有使用任何驗(yàn)證裝飾器的屬性的驗(yàn)證(返回的)對(duì)象 |
forbidNonWhitelisted | boolean | 如果設(shè)置為 true ,驗(yàn)證器不會(huì)去掉非白名單的屬性,而是會(huì)拋出異常 |
forbidUnknownValues | boolean | 如果設(shè)置為 true ,嘗試驗(yàn)證未知對(duì)象會(huì)立即失敗 |
disableErrorMessage | boolean | 如果設(shè)置為 true ,驗(yàn)證錯(cuò)誤不會(huì)返回給客戶端 |
errorHttpStatusCode | number | 這個(gè)設(shè)置允許你確定在錯(cuò)誤時(shí)使用哪個(gè)異常類型。默認(rèn)拋出 BadRequestException |
exceptionFactory | Function | 接受一個(gè)驗(yàn)證錯(cuò)誤數(shù)組并返回一個(gè)要拋出的異常對(duì)象 |
groups | string[] | 驗(yàn)證對(duì)象時(shí)使用的分組 |
always | boolean | 設(shè)置裝飾器選項(xiàng) always 的默認(rèn)值。默認(rèn)值可以在裝飾器的選項(xiàng)中被覆寫 |
strictGroups | boolean | 忽略在任何分組內(nèi)的裝飾器,如果 groups 沒有給出或者為空 |
dismissDefaultMessages | boolean | 如果設(shè)置為 true ,將不會(huì)使用默認(rèn)消息驗(yàn)證,如果不設(shè)置,錯(cuò)誤消息會(huì)始終是 undefined |
validationError.target | boolean | 確定目標(biāo)是否要在 ValidationError 中暴露出來 |
validationError.value | boolean | 確定驗(yàn)證值是否要在 ValidationError 中暴露出來 |
stopAtFirstError | boolean | 如果設(shè)置為 true ,對(duì)于給定的屬性的驗(yàn)證會(huì)在觸發(fā)第一個(gè)錯(cuò)誤之后停止。默認(rèn)為 false |
更多關(guān)于class-validator包的內(nèi)容見項(xiàng)目倉庫。
為了本教程的目的,我們將綁定 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;
}
錯(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。
來自網(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 。
在上述部分,我們演示了 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),
) {}
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
盡管本章展示了使用 HTTP 風(fēng)格的應(yīng)用程序的例子(例如,Express或 Fastify ), ValidationPipe 對(duì)于 WebSockets 和微服務(wù)是一樣的,不管使用什么傳輸方法。
要閱讀有關(guān)由 class-validator 提供的自定義驗(yàn)證器,錯(cuò)誤消息和可用裝飾器的更多信息,請(qǐng)?jiān)L問此頁面。
更多建議: