理解 TypeScript (NestJS) 中的数据传输对象 (DTO) 和数据验证
数据传输对象 (DTO) 是 NestJS 应用中数据验证的基础。DTO 为 NestJS 提供了多层次的灵活数据验证。
在本出版物中,您将深入了解数据传输对象,讨论验证机制、身份验证模型以及您需要了解有关数据传输对象的所有信息。
先决条件
本教程并非面向初学者。如果您刚开始使用 NestJS,请阅读这篇文章。
要充分理解本文,您需要满足以下要求。
- 具备使用 MongoDB 创建 NestJS 应用的经验。
- 熟练使用 PostMan(推荐)或其他 API 测试工具。
- 具备 bcrypt 或 NodeJS crypto 模块的基本知识。
一旦您满足这些要求,我们就可以开始了。
什么是数据传输对象?
数据传输对象(通常称为 DTO)是一种用于验证数据并定义发送到 Nest 应用程序的数据结构的对象。DTO 与接口类似,但与接口的区别在于以下几点:
- 接口用于类型检查和结构定义。
- DTO 用于类型检查、结构定义和数据验证。
- 接口在编译过程中会消失,因为它是 TypeScript 的原生接口,而 JavaScript 中不存在接口。
- DTO 使用原生 JavaScript 支持的类来定义,因此编译后仍然存在。
单独使用 DTO 只能进行类型检查和结构定义。要使用 DTO 运行数据验证,您需要使用 NestJS ValidationPipe。
验证机制
NestJS 使用管道来验证数据。通过管道的数据会被评估,如果通过验证,则原样返回;否则,抛出错误。
NestJS 有 8 个内置管道,但您将重点关注ValidationPipe使用该class-validator包的管道,因为它抽象了很多冗长的代码,并且可以使用装饰器轻松验证数据。
简单用户身份验证模型中的 DTO
用户身份验证模型是揭开数据转移对象 (DTO) 神秘面纱的绝佳例子。这是因为它需要多层数据验证。让我们创建一个这样的模型,并探索 DTO 以及如何ValidationPipe验证所有进入我们应用程序的数据形式。
搭建开发环境
设置开发环境:
- 使用 Nest CLI 创建一个新项目,
- 使用 CLI生成您的
Module、Service、 ,Controller - 安装
Mongoose应用程序并将其连接到数据库, - 创建您的模式文件夹并定义
Schema。
典型的用户认证模式如下所示:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type UserDocument = User & Document;
@Schema()
export class User {
@Prop({ required: true })
fullName: string;
@Prop({ required: true })
email: string;
@Prop({ required: true })
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
具有fullName、、email和password作为必需属性。
- 在你的模块内创建一个名为 `.dto` 的文件夹
dto。你的 DTO 类将存储并导出到这里。 - 在文件夹内创建一个文件
dto,并将其命名为user.dto.ts。
DTO的结构
DTO 是一种类,因此它遵循与类相同的语法。
export class newUserDto {
fullName: string;
email: string;
password: string;
}
以上是基本 DTO 的结构。
这种结构仅用于类型检查,不具备数据验证功能。
实施数据验证
要添加验证功能,请按照以下步骤操作;
- 在你里面
main.ts, {ValidationPipe}从以下来源导入'@nestjs/common':- 在函数内部
bootstrap,紧挨着常量下方app,调用该useGlobalPipes方法并将其作为参数app传递。new ValidationPipe()
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import * as dotenv from 'dotenv';
dotenv.config();
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT);
}
bootstrap();
这将启用自动验证,并确保所有端点免受错误数据的侵害。验证管道接受一个选项对象作为参数,您可以在官方文档中找到更多相关信息。
- 运行
class-validator以下命令安装该软件包:class-transformer
npm i --save class-validator class-transformer
该函数class-transformer可以将 JSON 对象转换为 DTO 类的实例,反之亦然。
该class-validator软件包包含大量验证查询,您可以在其文档中找到它们,但您将重点关注其中几个与用户数据验证相关的查询。在您的user.dto.ts文件中,从以下位置导入查询class-validator:
IsNotEmpty此验证器检查给定值是否为空。它接受一个选项对象作为参数。IsString此验证器检查给定值是否为真正的字符串。它接受一个选项对象作为参数。IsEmail此验证器检查给定值是否为电子邮件类型。它接受一个选项对象作为参数。Length此验证器接受 3 个参数。第一个参数是minLength,第二个参数是maxLength,最后一个参数是一个选项对象。
以上代码将用作装饰器来验证各自的字段。请在您的 DTO 中实现此功能。
import { IsNotEmpty, IsEmail, Length, IsString } from 'class-validator';
export class newUserDto {
@IsNotEmpty({ message: 'Please Enter Full Name' })
@IsString({ message: 'Please Enter Valid Name' })
fullName: string;
@IsEmail({ message: 'Please Enter a Valid Email' })
email: string;
@Length(6, 50, {
message: 'Password length Must be between 6 and 50 charcters',
})
password: string;
}
密码字段目前只有Length验证,但在实际生产环境中,您可能需要添加一些其他字段以确保密码更加安全。
这些新的验证字段可能包括检查密码是否包含大写字母、小写字母和数字。
这可以通过多种方式实现,包括:
- 添加更多验证装饰器,就像你在
fullName字段中所做的那样。 - 使用
@Matches验证器并将检查您的要求的正则表达式作为参数传递。
但最简洁的方法是创建一个自定义验证器来满足您的需求。您可以在这里了解更多关于自定义装饰器的信息。
发布和提交请求
POST通过向应用程序发送少量请求来测试数据验证PUT。但在此之前,以明文形式存储密码是一种糟糕的做法。因此,在将密码存储到数据库之前,请先对其进行哈希处理;
密码哈希
您可以使用名为 `crypto` 的包bcrypt或 NodeJS 的 `crypto` 模块对密码进行哈希处理。本文档涵盖了 `crypto` 的相关内容bcrypt。要安装,bcrypt请运行 `mash`。
npm i bcrypt
npm i -D @types/bcrypt
创建一个utils文件夹来存储包含实用函数的文件,例如用于对密码进行哈希处理的函数。
为了确保用户密码的安全,需要在将其存储到数据库之前对其进行哈希处理。让我们来实现这一点。
- 在你的文件夹中创建一个文件
utils,该文件将包含你的实用函数。 - 导入
*方式bcrypt'bcrypt' - 创建一个函数,该函数接收原始密码并对其进行哈希处理,
bcrypt然后返回哈希后的密码。导出该函数。
import * as bcrypt from 'bcrypt';
export async function hashPassword(textPassword: string) {
const salt = await bcrypt.genSalt();
const hash = await bcrypt.hash(textPassword, salt);
return hash;
}
该软件包有多种使用方法bcrypt,您可以在这里找到。
现在你已经有了对密码进行哈希处理的函数,请实现你的第一个POST请求。
创建新用户
要创建具有有效凭据的新用户,您需要将之前创建的 DTO 导入到您的服务中。
服务逻辑
- 创建一个
async函数newUser,该函数接收一个参数user,该参数应为新用户的数据。将其类型设置为之前创建的 DTO。 - 导入该
hashPassword函数。 - 在函数内部
async,创建一个常量,并将等待函数password返回的值作为参数赋值给该常量。hashPassworduser.password - 返回您自己的版本的 my 的新实例
userModel作为参数传入一个包含解构user和的对象password,并对其调用该save()方法。
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './schemas/user.schema';
import { newUserDto } from './dto/user.dto';
import { hashPassword } from './utilis/bcrypt.utils';
@Injectable()
export class AuthService {
constructor(
@InjectModel(User.name)
private userModel: Model<UserDocument>,
) {}
async newUser(user: newUserDto): Promise<User> {
const password = await hashPassword(user.password);
return await new this.userModel({ ...user, password }).save();
}
}
我们之前设置的 DTO 和验证器会不断检查数据是否有效,然后再将用户数据存储到数据库中。
实现控制器逻辑,以便您可以使用一些虚拟数据测试 DTO。
控制器逻辑
import {
Controller,
Body,
Post,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { newUserDto } from './dto/user.dto';
@Controller('users')
export class AuthController {
constructor(private readonly service: AuthService) {}
@Post('signup')
async createUser(
@Body()
user: newUserDto,
): Promise<newUserDto> {
return await this.service.newUser(user);
}
如上面的代码块所示,该user参数的类型为newUserDto。这确保了所有进入应用程序的数据都与 DTO 匹配,否则会抛出错误。
测试端点
http://localhost:3000/users/signup使用 Postman 或您喜欢的测试工具,通过发送请求,用一些虚拟数据测试验证结果。
{
"fullName":"Jon Snow",
"email":"snow@housestark.com",
"password":"password"
}
上面的 JSON 数据符合所有验证条件。因此,您将收到201包含数据、一个_id属性和一个哈希密码的响应。请复制该_id属性,在测试请求中的数据验证时需要用到它PUT。
{
"fullName":"Arya Stark",
"email":"aryathefaceless@housestark",
"password":"password"
}
上面的 JSON 数据包含一个无效email属性。因此,您将收到一条包含错误描述消息的响应。请将该属性400更新为 . 以将其保存到数据库中。emailaryathefaceless@housestark.com
{
"fullName":"Tyrion Lannister",
"email":"tyrion@houselannister.com",
"password":"pass",
"house":"Lannister"
}
上面的 JSON 数据存在 2 个问题,密码太短(少于 6 个字符),并且多了一个字段house。
增加请求长度password并再次发送请求。您将收到200包含已处理数据的请求,但该house字段不会存储在数据库中,因为它在通过验证管道时已被过滤。
更新用户
实现PUT按需更新用户数据的路由。
服务逻辑
async updateUser(id: string, userData: newUserDto): Promise<User> {
return await this.userModel.findByIdAndUpdate(id, userData);
}
控制器逻辑
@Put(':id')
async updateuser(
@Param('id')
id: string,
@Body()
user: newUserDto,
): Promise<newUserDto> {
return this.service.updateUser(id, user);
}
与请求类似POST,数据user也被赋予了类型newUserDto。因此,所有数据在保存到数据库之前都会经过彻底检查。
通过更新数据库中存储的某个用户来测试此端点。请记住,您复制的是_id属于 Jon Snow 的数据。
所以,请使用以下数据PUT发出请求;http://localhost:3000/id
{
"fullName":"Jon Snow",
"email":"snow@housetargaryen.com",
"password":"password"
}
以上数据符合所有验证条件,因此会返回200响应。如果任何数据字段包含无效数据,则会返回400响应,并且PUT请求会失败。
用户登录
如果无法实现用户登录,用户身份验证模型就不完整。要实现这一逻辑;
首先,你需要一个bcrypt实用函数来比较哈希密码和明文密码。像这样:
export async function validatePassword(textPassword: string, hash: string) {
const validUser = await bcrypt.compare(textPassword, hash);
return validUser;
}
接下来,您需要为登录数据创建一个新的 DTO。该 DTO 与之前的 DTO 类似,newUserDto但没有该fullName属性。
import { IsEmail } from 'class-validator';
export class loginUserDto {
@IsEmail({ message: 'Please Enter a Valid Email' })
email: string;
password: string;
}
请注意,该属性未添加验证器password。这是因为这样做可能会在未来造成安全威胁,因为它会将密码长度范围暴露给恶意用户。
服务逻辑
async loginUser(loginData: loginUserDto) {
const email = loginData.email;
const user = await this.userModel.findOne({ email });
if (user) {
const valid = await validatePassword(loginData.password, user.password);
if (valid) {
return user;
}
}
return new UnauthorizedException('Invalid Credentials');
}
您的服务逻辑应该能够做到:
检查用户是否存在;
- 如果用户存在,则验证密码。
- 如果用户不存在,则抛出异常。
验证密码;
- 如果密码匹配,则返回用户
- 如果密码不匹配,则抛出异常。
控制器逻辑
@Post('login')
async loginUser(
@Body()
loginData: loginUserDto,
) {
return await this.service.loginUser(loginData);
}
你的控制器应该向……发出POST请求http://localhost:3000/users/login。
结论
本文到此结束。以下是文章内容的总结。
- 什么是DTO?
- DTO 和接口之间的区别
- DTO的结构,
- NestJS验证机制
- 使用 bcrypt 创建一个简单的用户身份验证模型。
那真是相当多了,恭喜你走到这一步。
你可以在Github上找到代码。
祝您编程愉快!
文章来源:https://dev.to/davidekete/understanding-data-transfer-objects-dto-and-data-validation-in-typescript-nestjs-1j2b