## 驗證
驗證網絡應用中傳遞的任何數據是一種最佳實踐。為了自動驗證傳入請求, Nest 提供了幾個開箱即用的管道。
- `ValidationPipe`
- `ParseIntPipe`
- `ParseBoolPipe`
- `ParseArrayPipe`
- `ParseUUIDPipe`
`ValidationPipe` 使用了功能強大的 [class-validator](https://github.com/typestack/class-validator) 包及其聲明性驗證裝飾器。 `ValidationPipe` 提供了一種對所有傳入的客戶端有效負載強制執行驗證規則的便捷方法,其中在每個模塊的本地類或者 DTO 聲明中使用簡單的注釋聲明特定的規則。
### 概覽
在 [Pipes](https://docs.nestjs.com/pipes) 一章中,我們完成了構建簡化驗證管道的過程。為了更好地了解我們在幕后所做的工作,我們強烈建議您閱讀本文。在這里,我們將重點討論 `ValidationPipe` 的各種實際用例,并使用它的一些高級定制特性。
### 使用內置的`ValidationPipe`
在開始使用之前,我們先安裝依賴。
```shell
$ npm i --save class-validator class-transformer
```
> `ValidationPipe` 從 `@nestjs/common` 包導入。
由于此管道使用了 `class-validator` 和 `class-transformer` 庫,因此有許多可用的選項。通過傳遞給管道的配置對象來進行配置。依照下列內置的選項:
```typescript
export interface ValidationPipeOptions extends ValidatorOptions {
transform?: boolean;
disableErrorMessages?: boolean;
exceptionFactory?: (errors: ValidationError[]) => any;
}
```
所有可用的`class-validator`選項(繼承自`ValidatorOptions`接口):
| 選項 | 類型 | 描述 |
| ---------------------- | -------- | --------------------------------------------------------------------------------- |
| `enableDebugMessages` | `boolean` | 如果設置為 `true` ,驗證器會在出問題的時候打印額外的警告信息 |
| `skipUndefinedProperties` | `boolean` | 如果設置為 `true` ,驗證器將跳過對所有驗證對象中值為 `null` 的屬性的驗證 |
| `skipNullProperties` | `boolean` | 如果設置為 `true` ,驗證器將跳過對所有驗證對象中值為 `null` 或 `undefined` 的屬性的驗證 |
| `skipMissingProperties` | `boolean` | 如果設置為 `true` ,驗證器將跳過對所有驗證對象中缺失的屬性的驗證 |
| `whitelist` | `boolean` | 如果設置為 `true` ,驗證器將去掉沒有使用任何驗證裝飾器的屬性的驗證(返回的)對象 |
| `forbidNonWhitelisted` | `boolean` | 如果設置為 `true` ,驗證器不會去掉非白名單的屬性,而是會拋出異常 |
| `forbidUnknownValues` | `boolean` | 如果設置為 `true` ,嘗試驗證未知對象會立即失敗 |
| `disableErrorMessage` | `boolean` | 如果設置為 `true` ,驗證錯誤不會返回給客戶端 |
| `errorHttpStatusCode` | `number` | 這個設置允許你確定在錯誤時使用哪個異常類型。默認拋出 `BadRequestException` |
| `exceptionFactory` | `Function` | 接受一個驗證錯誤數組并返回一個要拋出的異常對象 |
| `groups` | `string[]` | 驗證對象時使用的分組 |
| `always` | `boolean` | 設置裝飾器選項 `always` 的默認值。默認值可以在裝飾器的選項中被覆寫 |
| `strictGroups` | `boolean` | 忽略在任何分組內的裝飾器,如果 `groups` 沒有給出或者為空 |
| `dismissDefaultMessages` | `boolean` | 如果設置為 `true` ,將不會使用默認消息驗證,如果不設置,錯誤消息會始終是 `undefined` |
| `validationError.target` | `boolean` | 確定目標是否要在 `ValidationError` 中暴露出來 |
| `validationError.value` | `boolean` | 確定驗證值是否要在 `ValidationError` 中暴露出來 |
| `stopAtFirstError` | `boolean` | 如果設置為 `true` ,對于給定的屬性的驗證會在觸發第一個錯誤之后停止。默認為 `false` |
> 更多關于`class-validator`包的內容見項目[倉庫](https://github.com/typestack/class-validator)。
### 自動驗證
為了本教程的目的,我們將綁定 `ValidationPipe` 到整個應用程序,因此,將自動保護所有接口免受不正確的數據的影響。
```typescript
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(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` 包提供的裝飾器來實現這一點,[這里](https://github.com/typestack/class-validator#validation-decorators)有詳細的描述。以這種方式,任何使用 `CreateUserDto` 的路由都將自動執行這些驗證規則。
```typescript
import { IsEmail, IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsNotEmpty()
password: string;
}
```
有了這些規則,當某人使用無效 `email` 執行對我們的接口的請求時,則應用程序將自動以 `400 Bad Request` 代碼以及以下響應正文進行響應:
```bash
{
"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';
}
```
與 `DTO` 一樣,`FindOneParams` 只是一個使用 `class-validator` 定義驗證規則的類。它是這樣的:
```typescript
import { IsNumberString } from 'class-validator';
export class FindOneParams {
@IsNumberString()
id: number;
}
```
### 禁用詳細錯誤
錯誤消息有助于解釋請求中的錯誤。然而,一些生產環境傾向于禁用詳細的錯誤。通過向 `ValidationPipe` 傳遞一個選項對象來做到這一點:
```typescript
app.useGlobalPipes(
new ValidationPipe({
disableErrorMessages: true,
})
);
```
現在,不會將錯誤消息返回給最終用戶。
### 剝離屬性
我們的 `ValidationPipe` 還可以過濾掉方法處理程序不應該接收的屬性。在這種情況下,我們可以對可接受的屬性進行**白名單**,白名單中不包含的任何屬性都會自動從結果對象中刪除。例如,如果我們的處理程序需要 `email` 和 `password`,但是一個請求還包含一個 `age` 屬性,那么這個屬性可以從結果 `DTO` 中自動刪除。要啟用這種行為,請將 `whitelist` 設置為 `true` 。
```typescript
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
})
);
```
當設置為 `true` 時,這將自動刪除非白名單屬性(在驗證類中沒有任何修飾符的屬性)。
或者,您可以在出現非白名單屬性時停止處理請求,并向用戶返回錯誤響應。要啟用此選項,請將 `forbidNonWhitelisted` 選項屬性設置為 `true` ,并將 `whitelist` 設置為 `true`。
### 負載對象轉換(Transform)
來自網絡的有效負載是普通的 JavaScript 對象。`ValidationPipe` 可以根據對象的 `DTO` 類自動將有效負載轉換為對象類型。若要啟用自動轉換,請將 `transform` 設置為 `true`。這可以在方法級別使用:
> cats.control.ts
```typescript
@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`會自動將 `string` 類型轉換為 `number` 。
### 顯式轉換
在上述部分,我們演示了 `ValidationPipe` 如何基于期待類型隱式轉換查詢和路徑參數,然而,這一特性需要開啟自動轉換功能。
可選地(在不開啟自動轉換功能的情況下),你可以使用 `ParseIntPipe` 或者 `ParseBoolPipe` 顯式處理值(注意,沒有必要使用 `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';
}
```
> `ParseIntPipe`和`ParseBoolPipe`從`@nestjs/common`包中導出。
### 映射類型
當你在編寫如**增刪改查**(新增/刪除/修改/查詢)的新功能的時候,你會經常基于一個實體類型來構造一個變種。 Nest 提供了一些可以進行類型轉換的功能函數來讓這種任務更加方便。
> 如果你的應用使用了 `@nestjs/swagger` 包,請看[這一章節](https://docs.nestjs.com/openapi/mapped-types)來了解更多有關映射類型的信息。類似地,如果你使用了 `@nestjs/graphql` 包請看[這一章節](https://docs.nestjs.com/graphql/mapped-types)。這幾個包都十分依賴類型所以需要分開導入以使用。因此,如果你使用了 `@nestjs/mapped-types` (而不是合適的包,根據你應用的類型是 `@nestjs/swagger` 或者 `@nestjs/graphql` ),你可能會碰到各種各樣的沒有被文檔記錄的副作用。
當構造輸入驗證類型(也稱為 DTO )時,你往往會在同一個類型上構造 **創建** 和 **更新** 變種。舉個例子, **創建** 變種可能要求全部的字段都被填寫,但是 **更新** 變種可能會把全部的字段變成可選的。
Nest 提供了 `PartialType()` 函數來讓這個任務變得簡單,同時也可以減少樣板代碼。
`PartialType()` 函數返回一個類型(一個類)包含被設置成可選的所有輸入類型的屬性。假設我們有一個 **創建** 的類型:
```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` 包導入的。
這些映射類型函數是可以組合的。下面的例子會創造一個擁有除了 `name` 屬性以外所有的 `CreateCatDto` 的屬性,而且這些屬性是可選的:
```typescript
export class UpdateCatDto extends PartialType(
OmitType(CreateCatDto, ['name'] as const),
) {}
```
### 轉換和驗證數組
TypeScript 不存儲泛型或接口的元數據,因此當你在 DTO 中使用它們的時候, `ValidationPipe` 可能不能正確驗證輸入數據。例如,在下列代碼中, `createUserDto` 不能正確驗證。
```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` 可能需要手動解析查詢參數。讓我們考慮一個返回作為查詢參數傳遞的標識的 `users` 的 `findByIds()` 方法:
```typescript
@Get()
findByIds(
@Query('id', new ParseArrayPipe({ items: Number, separator: ',' }))
ids: number[],
) {
return 'This action returns users by ids';
}
```
這個構造用于驗證一個來自如下形式帶參數的 `GET` 請求:
```typescript
GET /?ids=1,2,3
```
### Websockets 和 微服務
盡管本章展示了使用 `HTTP` 風格的應用程序的例子(例如,`Express`或 `Fastify` ), `ValidationPipe` 對于 `WebSockets` 和微服務是一樣的,不管使用什么傳輸方法。
### 學到更多
要閱讀有關由 `class-validator` 提供的自定義驗證器,錯誤消息和可用裝飾器的更多信息,請訪問[此頁面](https://github.com/typestack/class-validator)。
- 介紹
- 概述
- 第一步
- 控制器
- 提供者
- 模塊
- 中間件
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 自定義裝飾器
- 基礎知識
- 自定義提供者
- 異步提供者
- 動態模塊
- 注入作用域
- 循環依賴
- 模塊參考
- 懶加載模塊
- 應用上下文
- 生命周期事件
- 跨平臺
- 測試
- 技術
- 數據庫
- Mongo
- 配置
- 驗證
- 緩存
- 序列化
- 版本控制
- 定時任務
- 隊列
- 日志
- Cookies
- 事件
- 壓縮
- 文件上傳
- 流式處理文件
- HTTP模塊
- Session(會話)
- MVC
- 性能(Fastify)
- 服務器端事件發送
- 安全
- 認證(Authentication)
- 授權(Authorization)
- 加密和散列
- Helmet
- CORS(跨域請求)
- CSRF保護
- 限速
- GraphQL
- 快速開始
- 解析器(resolvers)
- 變更(Mutations)
- 訂閱(Subscriptions)
- 標量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 聯合類型
- 枚舉(Enums)
- 字段中間件
- 映射類型
- 插件
- 復雜性
- 擴展
- CLI插件
- 生成SDL
- 其他功能
- 聯合服務
- 遷移指南
- Websocket
- 網關
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 適配器
- 微服務
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定義傳輸器
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 獨立應用
- Cli
- 概述
- 工作空間
- 庫
- 用法
- 腳本
- Openapi
- 介紹
- 類型和參數
- 操作
- 安全
- 映射類型
- 裝飾器
- CLI插件
- 其他特性
- 遷移指南
- 秘籍
- CRUD 生成器
- 熱重載
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模塊
- Swagger
- 健康檢查
- CQRS
- 文檔
- Prisma
- 靜態服務
- Nest Commander
- 問答
- Serverless
- HTTP 適配器
- 全局路由前綴
- 混合應用
- HTTPS 和多服務器
- 請求生命周期
- 常見錯誤
- 實例
- 遷移指南
- 發現
- 誰在使用Nest?