六、数据校验那点事(nestjs+next.js从零开始一步一步创建通用后台管理系统)

260 阅读7分钟

在 NestJS 中,验证数据是一种常见的需求,以确保接收到的数据符合预期的格式和规则。

NestJS 提供了内置的验证管道(ValidationPipe),它可以在控制器接收到请求数据后,对数据进行验证。这个管道会根据你定义的验证规则(通常是通过类和装饰器来定义)来检查数据的有效性。

验证管道可以全局开启也可以在控制器或路由上启用。全局开启时传输的dto对象的验证规则自动进行验证和字段类型转换。在控制器或路由上启用验证管道可以除了传输对象外对一些参数做特殊验证,如删除用户时id号是参数传入的,可以使用ParseIntPipe进行验证。

验证器包括对象验证器或类验证器,如在控制器中使用zod对象对传入的参数对象进行验证,但这种方式无法全局通用,所以nestjs官方建议使用类验证器,在dto对象上定义验证规则,不用在控制器中再对dto对象进行验证,这很符合DRY设计原则。

下面就使用类验证器为例说明数据验证的使用方法,包括:

1、基础用法

1.1、启用全局验证管道

1.2、配置类验证规则

1.3、在控制器或路由上启用验证管道

2、进阶用法

2.1、自定义验证

2.2、dto验证规则的一些特殊写法


正文

1、基础用法

1.1、启用全局验证管道

    启用全局验证有两种方式,一种是在main.ts中的应用上启用全局验证管道,一种是在app.module模块中启用全局验证管道。但第一种方式如果验证器中存在一些依赖,系统无法自动注入,所以推荐在app.module中启用全局验证。

    在main.ts中启用验证代码:

async function bootstrap() {
  const app = await NestFactory.create(AppModule); 
  //启用全局验证
  app.useGlobalPipes(new ValidationPipe());
  // 启用自定义配置项的全局验证管道
  //app.useGlobalPipes(
  //  new ValidationPipe({
  //    transform: true, // 启用数据转换
  //    whitelist: true, // 忽略 DTO 中未定义的属性
  //    forbidNonWhitelisted: true, // 如果启用了 whitelist,禁止额外的属性
  //  }),
  //);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

在主模块(app.module.ts)的提供者(providers)中全局启用验证管道:

 import { Module } from '@nestjs/common';
    import { APP_PIPE } from '@nestjs/core';
    import { ValidationPipe } from '@nestjs/common';

    @Module({
      providers: [
        {
          provide: APP_PIPE,
          useValue: new ValidationPipe({
            // 配置选项
            transform: true, // 将请求数据转换为目标类型(如从字符串转换为数字等)
            whitelist: true, // 忽略 DTO 中没有定义的属性
            forbidNonWhitelisted: true, // 如果启用了 whitelist,对于额外的属性会返回 400 错误
            // 其他选项...
          }),
        },
      ],
    })
    export class AppModule {}

1.2、配置类验证规则

使用 class-validator 装饰器对dto进行配置:

1)、首先要安装依赖:

pnpm i --save class-validator class-transformer

2)、创建一个dto对象

import { IsString, IsEmail, MinLength, MaxLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(3)
  @MaxLength(50)
  username: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}

在这个 DTO 中,使用了 class - validator 的装饰器来定义验证规则。@IsString() 验证字段是否为字符串,@MinLength(3)@MaxLength(50) 验证 username 字段的长度范围,@IsEmail() 验证 email 字段是否符合电子邮件格式。

3)、在控制器中使用这个dto:

在控制器的方法中,将 DTO 作为参数类型,这样验证管道就会自动对传入的数据进行验证在控制器的方法中,将 DTO 作为参数类型,这样验证管道就会自动对传入的数据进行验证。

当客户端发送 POST 请求到 /users 端点,并且请求体不符合 CreateUserDto 中定义的验证规则时,验证管道会返回一个 400 错误,包含验证失败的详细信息。

import { Controller, Post, Body } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    // 处理用户创建逻辑
  }
}

1.3、在控制器或路由上启用验证管道

在控制器上启用验证:

使用 @UsePipes 装饰器在控制器类上,管道会作用于该控制器中的所有路由方法。

import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
@UsePipes(new ValidationPipe()) // 在控制器级别启用验证管道
export class UsersController {
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    // 处理用户创建逻辑
  }

  @Post('another-route')
  anotherRoute(@Body() createUserDto: CreateUserDto) {
    // 处理另一个路由的逻辑
  }
}

对单个路由启用验证管道:

使用 @UsePipes 装饰器在特定的路由方法上,管道只作用于该方法。

import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  @Post()
  @UsePipes(new ValidationPipe()) // 在单个路由上启用验证管道
  create(@Body() createUserDto: CreateUserDto) {
    // 处理用户创建逻辑
  }

  @Post('another-route')
  anotherRoute(@Body() createUserDto: CreateUserDto) {
    // 验证管道不会自动应用到这里,除非也添加了 @UsePipes 装饰器
  }
}

2、进阶用法

2.1、自定义验证装饰器

自定义验证装饰器可以通过 registerDecorator 函数来创建,它允许你定义自己的验证逻辑和错误消息。

1)、创建自定义验证装饰器

有时候内置的验证装饰器可能无法满足复杂的业务规则,这时就需要创建自定义的验证装饰器。

可以通过 registerDecorator 函数来创建自定义装饰器。例如,创建一个验证密码强度的自定义装饰器:

 import { ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, registerDecorator } from 'class - validator';

    @ValidatorConstraint({ name: 'isPasswordStrong', async: false })
    export class IsPasswordStrongConstraint implements ValidatorConstraintInterface {
      validate(password: string, args: any) {
        // 自定义验证逻辑,例如检查密码是否包含大小写字母和数字
        const regex = /^(?=.*[a - z])(?=.*[A - Z])(?=.*\d).{8,}$/;
        return regex.test(password);
      }

      defaultMessage(args: any): string {
        return 'Password must contain at least one uppercase letter, one lowercase letter, and one number, and be at least 8 characters long.';
      }
    }

    export function IsPasswordStrong(validationOptions?: ValidationOptions) {
      return function (object: object, propertyName: string) {
        registerDecorator({
          target: object.constructor,
          propertyName: propertyName,
          options: validationOptions,
          constraints: [],
          validator: IsPasswordStrongConstraint,
        });
      };
    }

2)、然后在 DTO 中使用这个自定义装饰器

import { IsString } from 'class - validator';
    import { IsPasswordStrong } from '../decorators/is - password - strong.decorator';

    export class CreateUserDto {
      @IsString()
      @MinLength(3)
      @MaxLength(50)
      username: string;

      @IsEmail()
      email: string;

      @IsString()
      @IsPasswordStrong()
      password: string;
    }

2.2、自定义验证类

自定义验证类通过实现 ValidatorConstraintInterface 接口来定义复杂的验证逻辑,并使用 @Validate 装饰器将该验证类应用到需要验证的属性上。

1)、创建自定义验证类: 密码匹配验证类

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ name: 'passwordMatch', async: false })
class PasswordMatchConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const [relatedPropertyName] = args.constraints;
    const relatedValue = (args.object as any)[relatedPropertyName];
    return value === relatedValue;
  }

  defaultMessage(args: ValidationArguments) {
    const [relatedPropertyName] = args.constraints;
    return `${args.property}${relatedPropertyName} 不匹配`;
  }
}

2)、 在dto中使用这个验证类

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

export class ChangePasswordDto {
  @IsString()
  @MinLength(8)
  password: string;

  @IsString()
  @MinLength(8)
  @Validate(PasswordMatchConstraint, ['password']) // 使用自定义验证类
  confirmPassword: string;
}
  • 自定义验证装饰器:使用 registerDecorator 创建,适合简单的验证逻辑和特定的验证场景。
  • 自定义验证类:通过实现 ValidatorConstraintInterface 创建,适合复杂的验证逻辑,可以复用验证代码。

这两种方式都可以很好地扩展 class-validator 的功能,满足项目的个性化验证需求。

2.3、dto验证规则的一些特殊写法

1)、条件验证

使用@ValidateIf装饰器可以根据特定条件来决定是否对某个属性进行验证,适合处理属性之间存在依赖关系的场景。

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

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

  @ValidateIf(o => o.email)
  @IsNotEmpty({ message: '新邮件地址不能为空' })
  newEmail: string;
}

2)、嵌套对象验证

如果DTO中包含嵌套对象,可以使用@ValidateNested()装饰器结合class-transformer的@Type()装饰器来进行验证。

import { Type } from 'class-transformer';
import { ValidateNested, IsString, IsNotEmpty } from 'class-validator';

class AddressDto {
  @IsString()
  @IsNotEmpty()
  street: string;

  @IsString()
  @IsNotEmpty()
  city: string;
}

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  username: string;

  @ValidateNested()
  @Type(() => AddressDto)
  address: AddressDto;
}

3)、分组验证

通过在验证装饰器中指定groups选项,可以对不同的属性进行分组,然后在使用验证管道时指定要验证的组,从而实现对不同场景下不同字段的验证。

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

export class CreateUserDto {
  @IsString()
  @IsNotEmpty({ groups: ['create'] })
  username: string;

  @IsString()
  @IsNotEmpty({ groups: ['create', 'update'] })
  password: string;
}

// 使用时指定组
const createUserValidationPipe = new ValidationPipe({ groups: ['create'] });
const updateUserValidationPipe = new ValidationPipe({ groups: ['update'] });

2.4、自定义验证错误处理

可以通过创建自定义的异常过滤器来处理验证错误。例如,自定义一个异常过滤器来返回更符合业务需求的错误格式。如果你已经参考第四章使用了统一的错误处理,可以不用阅读本节内容。

import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
    import { Request, Response } from 'express';

    @Catch(HttpException)
    export class ValidationExceptionFilter implements ExceptionFilter {
      catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
        const status = exception.getStatus();

        const errorResponse = {
          statusCode: status,
          timestamp: new Date().toISOString(),
          path: request.url,
          message: exception.message,
        };

        response.status(status).json(errorResponse);
      }
    }

然后在主模块中注册这个过滤器:

providers: [
        {
          provide: APP_FILTER,
          useClass: ValidationExceptionFilter,
        },
      ],

这样就可以自定义验证错误的返回格式,使其符合 API 设计规范或者业务需求。

在实际开发中,可以根据项目的需求灵活地使用这些验证功能,确保传入的数据是合法和有效的。

3、测试:

dto中用户和密码长度不能小于10。 image.png

postman测试如下:

image.png 可见用户名和密码两个错误不会同时返回。 另外如果没有自定义错误消息,则系统自动返回默认消息内容,见下图用户名超过12个字符后的错误消息:

image.png