1-4 接口参数校验:NestJS 基于装饰器的类验证器管道
本节将实现从手动 if-else 校验到声明式装饰器校验的完整升级。核心是安装 class-validator 和 class-transformer,创建 DTO 类并配置全局 ValidationPipe,让参数校验自动化、声明化。
核心工具库介绍
管道校验依赖两个第三方库,它们分工明确:
| 库 | 职责 | 安装命令 |
|---|---|---|
class-transformer | 将普通 JS 对象转换为 class 实例(plainToInstance) | pnpm add class-transformer |
class-validator | 基于装饰器对 class 属性进行校验 | pnpm add class-validator |
pnpm add class-validator class-transformer
bash
工作流程:
HTTP 请求体(JSON 字符串)
│
▼ JSON.parse()
普通 JS 对象 { username: "tom", password: "123" }
│
▼ class-transformer (plainToInstance)
DTO class 实例 (SignInUserDto)
│
▼ class-validator (validate)
┌──────┴──────┐
│ │
校验通过 校验失败
│ │
▼ ▼
Controller 抛出 BadRequestException
继续处理 返回 400 错误
text
第一步:全局注册 ValidationPipe
在应用入口 main.ts 中注册全局校验管道:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局类验证管道
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 剥离 DTO 中未定义的属性,提升接口安全性
}),
);
app.setGlobalPrefix('api/v1');
await app.listen(3000);
}
bootstrap();
typescript
whitelist: true 的安全意义:
| 场景 | whitelist: false | whitelist: true |
|---|---|---|
请求体包含额外字段 { username, password, role: "admin" } | 全部传递给 Controller | role 被自动剥离 |
攻击者尝试注入 isAdmin: true | 可能被写入数据库 | 自动移除,无法注入 |
whitelist: true是生产环境的必备配置,防止客户端传入未预期的字段。
第二步:创建 DTO 并定义校验规则
在 auth/dto/ 目录下创建 DTO 文件:
// auth/dto/sign-in-user.dto.ts
import {
IsNotEmpty,
IsString,
Length,
ValidationArguments,
} from 'class-validator';
export class SignInUserDto {
@IsNotEmpty({ message: '用户名不得为空' })
@IsString({ message: '用户名必须是一个字符串' })
@Length(6, 20, {
message: (args: ValidationArguments) => {
return `用户名长度必须是 ${args.constraints[0]} 到 ${args.constraints[1]} 位的字符`;
},
})
username: string;
@IsNotEmpty({ message: '密码不得为空' })
@IsString({ message: '密码必须是一个字符串' })
@Length(6, 32, {
message: (args: ValidationArguments) => {
return `密码长度必须是 ${args.constraints[0]} 到 ${args.constraints[1]} 位的字符`;
},
})
password: string;
}
typescript
常用 class-validator 装饰器一览:
| 装饰器 | 功能 | 示例 |
|---|---|---|
@IsNotEmpty() | 非空校验 | @IsNotEmpty({ message: '不得为空' }) |
@IsString() | 字符串类型校验 | @IsString({ message: '必须是字符串' }) |
@Length(min, max) | 长度范围校验 | @Length(6, 20) |
@MinLength(min) | 最小长度 | @MinLength(6) |
@MaxLength(max) | 最大长度 | @MaxLength(32) |
@IsEmail() | 邮箱格式 | @IsEmail({}, { message: '邮箱格式不正确' }) |
@Matches(regex) | 正则匹配 | @Matches(/^[a-zA-Z]/, { message: '必须字母开头' }) |
@IsDate() | 日期类型 | @IsDate() |
@IsBoolean() | 布尔类型 | @IsBoolean() |
@IsOptional() | 可选字段 | @IsOptional() 配合其他校验器使用 |
第三步:在 Controller 中使用 DTO
将 Controller 参数类型从内联对象替换为 DTO 类:
// auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignInUserDto } from './dto/sign-in-user.dto';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('signin')
signIn(@Body() signInDto: SignInUserDto) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
@Post('signup')
signUp(@Body() signUpDto: SignInUserDto) {
return this.authService.signUp(signUpDto.username, signUpDto.password);
}
}
typescript
使用 DTO 后,Controller 中的手动 if-else 校验代码可以全部删除,校验逻辑由管道自动完成。
自定义错误消息
class-validator 支持三种自定义消息方式:
方式一:静态字符串
@Length(6, 20, { message: '用户名长度必须为6到20位' })
username: string;
typescript
方式二:动态消息(使用 ValidationArguments)
@Length(6, 20, {
message: (args: ValidationArguments) => {
return `用户名长度必须是 ${args.constraints[0]} 到 ${args.constraints[1]} 位的字符`;
},
})
username: string;
typescript
ValidationArguments 提供以下属性:
| 属性 | 说明 |
|---|---|
value | 当前校验的属性值 |
constraints | 装饰器参数(如 @Length(6, 20) 中的 [6, 20]) |
targetName | 类名 |
object | 被校验的对象实例 |
property | 属性名 |
自定义校验装饰器
当内置装饰器无法满足需求时,可以通过 registerDecorator 创建自定义校验规则:
// common/decorators/is-longer-than.decorator.ts
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
export function IsLongerThan(
property: string,
validationOptions?: ValidationOptions,
) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isLongerThan',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return (
typeof value === 'string' &&
typeof relatedValue === 'string' &&
value.length > relatedValue.length
);
},
},
});
};
}
typescript
使用方式:
export class SignUpDto {
@MinLength(6)
password: string;
@IsLongerThan('password', {
message: '确认密码必须比密码长',
})
confirmPassword: string;
}
typescript
错误响应示例
当校验失败时,NestJS 自动返回结构化的错误信息:
{
"statusCode": 400,
"message": [
"用户名长度必须是 6 到 20 位的字符",
"用户名必须是一个字符串"
],
"error": "Bad Request"
}
json
多层校验的触发顺序: 同一属性上多个装饰器按声明顺序依次校验,所有失败的错误消息会合并返回。
从手动校验到装饰器校验的对比
// 之前:手动 if-else 校验(冗长、难以维护)
@Post('signup')
signUp(@Body() dto: any) {
if (!dto.username || !dto.password) {
throw new HttpException('用户名密码不得为空', HttpStatus.BAD_REQUEST);
}
if (dto.username.length < 6 || dto.username.length > 16) {
throw new HttpException('用户名长度不符合要求', HttpStatus.BAD_REQUEST);
}
// ... 更多校验逻辑
return this.authService.signUp(dto.username, dto.password);
}
// 之后:装饰器声明式校验(简洁、可维护)
@Post('signup')
signUp(@Body() signUpDto: SignInUserDto) {
return this.authService.signUp(signUpDto.username, signUpDto.password);
}
typescript
本节要点
- 安装依赖:
pnpm add class-validator class-transformer安装校验和转换工具 - 全局注册 ValidationPipe:在
main.ts中通过app.useGlobalPipes()配置,whitelist: true提升安全性 - DTO 定义校验规则:使用
@IsNotEmpty、@IsString、@Length等装饰器声明校验逻辑 - 自定义消息:支持静态字符串和
ValidationArguments动态消息 - 自定义装饰器:通过
registerDecorator扩展校验能力 - 安全收益:
whitelist配置自动剥离非法字段,防止参数注入攻击
↑