无论是在分锅大会上预防小人还是为了保证webserver安全,参数验证是确保应用程序安全和稳定运行的重要环节。无论是API请求参数、表单数据,还是配置文件,验证都能有效防止恶意输入,确保数据的完整性和正确性。
本文将详细介绍几种流行的参数验证库:Joi、Ajv、Zod、Yup、Validator.js、Superstruct、Class-validator,并通过综合实例展示如何在HTTP请求中使用这些库进行参数验证。
为什么验证参数
- 安全性:防止SQL注入、XSS攻击等恶意输入。
- 数据完整性:确保数据符合预期格式和类型。
- 用户体验:及时反馈错误信息,提高用户体验。
- 代码健壮性:减少运行时错误,提升代码稳定性。
验证库概览
| 名称 | GitHub 地址 | Stars 数量 | 是否支持 TypeScript | 功能简述 |
|---|---|---|---|---|
| Joi | github.com/hapijs/joi | 20k+ | 是 | 强大的对象模式描述语言和验证器 |
| Ajv | github.com/ajv-validat… | 12k+ | 是 | JSON Schema 验证器,支持JSON格式的验证 |
| Zod | github.com/colinhacks/… | 33k+ | 是 | TypeScript优先的模式验证库 |
| Yup | github.com/jquense/yup | 22k+ | 是 | JavaScript对象模式验证和解析库 |
| Validator.js | github.com/validatorjs… | 23k+ | 否 | 字符串验证库,支持多种常见验证 |
| Superstruct | github.com/ianstormtay… | 7k+ | 是 | 用于验证 JavaScript 数据结构的库 |
| Class-validator | github.com/typestack/c… | 10k+ | 是 | 基于装饰器的验证库,适用于TypeScript类 |
各库详细介绍
1. Joi
优势:
- 功能强大,支持复杂的验证规则。
- 直观的API设计,易于使用。
- 支持链式调用和嵌套对象验证。
劣势:
- 体积较大。
- 学习曲线较陡。
使用方式:
const Joi = require('joi');
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(),
email: Joi.string().email().required()
});
const { error, value } = schema.validate({ username: 'abc', password: '123456', email: 'abc@example.com' });
if (error) {
console.error(error.details);
}
适用场景:
- 需要复杂验证规则的场景。
- 需要嵌套对象验证的场景。
2. Ajv
优势:
- 速度快,性能高。
- 支持JSON Schema标准。
- 可扩展性强,支持自定义关键字。
劣势:
- JSON Schema学习成本较高。
- 错误信息不够友好。
使用方式:
const Ajv = require('ajv');
const ajv = new Ajv();
const schema = {
type: 'object',
properties: {
username: { type: 'string', minLength: 3, maxLength: 30 },
password: { type: 'string', pattern: '^[a-zA-Z0-9]{3,30}$' },
email: { type: 'string', format: 'email' }
},
required: ['username', 'password', 'email'],
additionalProperties: false
};
const validate = ajv.compile(schema);
const valid = validate({ username: 'abc', password: '123456', email: 'abc@example.com' });
if (!valid) {
console.error(validate.errors);
}
适用场景:
- 需要高性能验证的场景。
- 需要遵循JSON Schema标准的场景。
3. Zod
优势:
- TypeScript优先,类型推断优秀。
- API简洁,易于使用。
- 支持链式调用和嵌套对象验证。
劣势:
- 功能相对较少。
- 生态系统不如其他库丰富。
使用方式:
import { z } from 'zod';
const schema = z.object({
username: z.string().min(3).max(30),
password: z.string().regex(/^[a-zA-Z0-9]{3,30}$/),
email: z.string().email()
});
try {
schema.parse({ username: 'abc', password: '123456', email: 'abc@example.com' });
} catch (e) {
console.error(e.errors);
}
适用场景:
- TypeScript项目。
- 需要类型推断的场景。
4. Yup
优势:
- API设计直观,易于使用。
- 支持链式调用和嵌套对象验证。
- 与Formik等表单库集成良好。
劣势:
- 性能稍逊于Ajv。
- 错误信息不够详细。
使用方式:
const yup = require('yup');
const schema = yup.object().shape({
username: yup.string().min(3).max(30).required(),
password: yup.string().matches(/^[a-zA-Z0-9]{3,30}$/).required(),
email: yup.string().email().required()
});
schema.validate({ username: 'abc', password: '123456', email: 'abc@example.com' })
.catch(err => {
console.error(err.errors);
});
适用场景:
- 前端表单验证。
- 需要简单易用的验证库。
5. Validator.js
优势:
- 轻量级。
- 支持多种常见验证。
- 易于集成到其他库中。
劣势:
- 只支持字符串验证。
- 不支持嵌套对象验证。
使用方式:
const validator = require('validator');
const isValidUsername = validator.isLength('abc', { min: 3, max: 30 });
const isValidPassword = validator.matches('123456', /^[a-zA-Z0-9]{3,30}$/);
const isValidEmail = validator.isEmail('abc@example.com');
if (!isValidUsername || !isValidPassword || !isValidEmail) {
console.error('Invalid input');
}
适用场景:
- 需要简单字符串验证的场景。
- 轻量级项目。
6. Superstruct
优势:
- 轻量级。
- API设计简洁。
- 支持嵌套对象验证。
劣势:
- 功能相对较少。
- 社区支持较少。
使用方式:
const { struct } = require('superstruct');
const User = struct({
username: 'string & min:3 & max:30',
password: 'string & pattern:/^[a-zA-Z0-9]{3,30}$/',
email: 'string & email'
});
try {
User({ username: 'abc', password: '123456', email: 'abc@example.com' });
} catch (e) {
console.error(e.message);
}
适用场景:
- 需要轻量级验证库的场景。
- 需要嵌套对象验证的场景。
7. Class-validator
优势:
- 基于装饰器,适用于TypeScript类。
- 与TypeORM等库集成良好。
- 支持复杂验证规则。
劣势:
- 仅适用于TypeScript。
- 学习曲线较陡。
使用方式:
import { IsEmail, IsString, Length, Matches, validate } from 'class-validator';
class User {
@IsString()
@Length(3, 30)
username: string;
@IsString()
@Matches(/^[a-zA-Z0-9]{3,30}$/)
password: string;
@IsEmail()
email: string;
}
const user = new User();
user.username = 'abc';
user.password = '123456';
user.email = 'abc@example.com';
validate(user).then(errors => {
if (errors.length > 0) {
console.error(errors);
}
});
适用场景:
- TypeScript项目。
- 需要类装饰器的场景。
综合实例:HTTP请求参数验证
1. 新建文件夹和初始化项目
首先,创建一个新的文件夹并初始化一个新的Node.js项目:
mkdir ts-validation-example
cd ts-validation-example
npm init -y
package.jsong
{
"name": "ts-validation-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "tsc && node dist/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"axios": "^1.7.7",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"express": "^4.21.0",
"joi": "^17.13.3",
"reflect-metadata": "^0.2.2",
"superstruct": "^2.0.2",
"validator": "^13.12.0",
"yup": "^1.4.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.5.5",
"@types/validator": "^13.12.1",
"ts-node": "^10.9.2",
"typescript": "^5.6.2"
}
}
2. 安装必要的依赖
接下来,安装Express和各个验证库的依赖,以及TypeScript和类型定义:
npm install express joi ajv zod yup validator superstruct class-validator reflect-metadata axios class-transformer ajv-formats
npm install --save-dev typescript ts-node @types/node @types/express @types/validator
3. 初始化 TypeScript 配置
初始化ts
tsc --init
配置如下
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
4. 创建项目结构和代码文件
创建项目结构和 server.ts 文件:
mkdir src
touch src/server.ts
在 src/server.ts 文件中,编写以下代码:
import express, { Request, Response } from "express";
import Joi from "joi";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import { z } from "zod";
import * as yup from "yup";
import validator from "validator";
import { assert, object, string, define } from "superstruct";
import "reflect-metadata";
import { plainToInstance } from "class-transformer";
import {
IsEmail,
IsString,
Length,
Matches,
validate as classValidate,
} from "class-validator";
import fs from "fs";
import path from "path";
const app = express();
app.use(express.json());
function getHrTimeInMicroseconds() {
const [seconds, nanoseconds] = process.hrtime();
return seconds * 1e6 + nanoseconds / 1e3;
}
app.post("/register", async (req: Request, res: Response) => {
const body = req.body;
console.log("收到请求体:", body);
let errors: any[] = [];
const validationTimes: { [key: string]: number } = {};
// Joi
const joiStart = getHrTimeInMicroseconds();
const joiSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string()
.pattern(new RegExp("^[a-zA-Z0-9]{3,30}$"))
.required(),
email: Joi.string().email().required(),
});
const { error: joiError } = joiSchema.validate(body);
validationTimes["Joi"] = getHrTimeInMicroseconds() - joiStart;
console.log("Joi 验证结果:", joiError ? joiError.details : "无错误");
if (joiError) errors.push({ 库: "Joi", 细节: joiError.details });
// Ajv
const ajvStart = getHrTimeInMicroseconds();
const ajv = new Ajv({ allErrors: true });
addFormats(ajv); // 添加这一行以引入格式验证
const ajvSchema = {
type: "object",
properties: {
username: { type: "string", minLength: 3, maxLength: 30 },
password: { type: "string", pattern: "^[a-zA-Z0-9]{3,30}$" },
email: { type: "string", format: "email" },
},
required: ["username", "password", "email"],
additionalProperties: false,
};
const validate = ajv.compile(ajvSchema);
const valid = validate(body);
validationTimes["Ajv"] = getHrTimeInMicroseconds() - ajvStart;
console.log("Ajv 验证结果:", valid ? "无错误" : validate.errors);
if (!valid) errors.push({ 库: "Ajv", 细节: validate.errors });
// Zod
const zodStart = getHrTimeInMicroseconds();
const zodSchema = z
.object({
username: z.string().min(3).max(30),
password: z.string().regex(/^[a-zA-Z0-9]{3,30}$/),
email: z.string().email(),
})
.strict();
try {
zodSchema.parse(body);
validationTimes["Zod"] = getHrTimeInMicroseconds() - zodStart;
console.log("Zod 验证结果: 无错误");
} catch (e: any) {
validationTimes["Zod"] = getHrTimeInMicroseconds() - zodStart;
console.log("Zod 验证结果:", e.errors);
errors.push({ 库: "Zod", 细节: e.errors });
}
// Yup
const yupStart = getHrTimeInMicroseconds();
const yupSchema = yup
.object()
.shape({
username: yup.string().min(3).max(30).required(),
password: yup
.string()
.matches(/^[a-zA-Z0-9]{3,30}$/)
.required(),
email: yup.string().email().required(),
})
.noUnknown(true, { message: "不允许未知字段" });
try {
await yupSchema.validate(body, { abortEarly: false });
validationTimes["Yup"] = getHrTimeInMicroseconds() - yupStart;
console.log("Yup 验证结果: 无错误");
} catch (e: any) {
validationTimes["Yup"] = getHrTimeInMicroseconds() - yupStart;
console.log("Yup 验证结果:", e.errors);
errors.push({ 库: "Yup", 细节: e.errors });
}
// Validator.js
const validatorStart = getHrTimeInMicroseconds();
const isValidUsername = validator.isLength(body.username as string, {
min: 3,
max: 30,
});
const isValidPassword = validator.matches(
body.password as string,
/^[a-zA-Z0-9]{3,30}$/
);
const isValidEmail = body.email && validator.isEmail(body.email as string);
validationTimes["Validator.js"] = getHrTimeInMicroseconds() - validatorStart;
console.log("Validator.js 验证结果:", {
isValidUsername,
isValidPassword,
isValidEmail,
});
if (!isValidUsername || !isValidPassword || !isValidEmail) {
errors.push({ 库: "Validator.js", 细节: "输入无效" });
}
// Superstruct
const superstructStart = getHrTimeInMicroseconds();
const Username = define("Username", (value: unknown) => {
return typeof value === "string" && value.length >= 3 && value.length <= 30;
});
const Password = define("Password", (value: unknown) => {
return typeof value === "string" && /^[a-zA-Z0-9]{3,30}$/.test(value);
});
const Email = define("Email", (value: unknown) => {
return typeof value === "string" && validator.isEmail(value as string);
});
const User = object({
username: Username,
password: Password,
email: Email,
});
try {
assert(body, User);
validationTimes["Superstruct"] =
getHrTimeInMicroseconds() - superstructStart;
console.log("Superstruct 验证结果: 无错误");
} catch (e: any) {
validationTimes["Superstruct"] =
getHrTimeInMicroseconds() - superstructStart;
console.log("Superstruct 验证结果:", e.message);
errors.push({ 库: "Superstruct", 细节: e.message });
}
// Class-validator
const classValidatorStart = getHrTimeInMicroseconds();
class UserClass {
@IsString()
@Length(3, 30)
username!: string;
@IsString()
@Matches(/^[a-zA-Z0-9]{3,30}$/)
password!: string;
@IsEmail()
email!: string;
}
const user = plainToInstance(UserClass, body);
const classErrors = await classValidate(user);
validationTimes["Class-validator"] =
getHrTimeInMicroseconds() - classValidatorStart;
console.log(
"Class-validator 验证结果:",
classErrors.length > 0 ? classErrors : "无错误"
);
if (classErrors.length > 0) {
errors.push({ 库: "Class-validator", 细节: classErrors });
}
// 记录验证执行时间
const logFilePath = path.join(__dirname, "validation_times.log");
fs.appendFileSync(logFilePath, JSON.stringify(validationTimes) + "\n");
if (errors.length > 0) {
return res.status(400).json({ 错误: errors });
}
res.status(200).json({ 消息: "注册成功" });
});
app.listen(3000, () => {
console.log("服务器运行在端口 3000");
});
5. 编译和运行项目
编译 TypeScript 文件并运行项目:
npx tsc
node dist/server.js
访问 http://localhost:3000/register,并发送POST请求来测试验证功能。
6. 测试接口
在根目录中新建testRegister.js文件来做一些测试
const axios = require("axios");
const api = axios.create({
baseURL: "http://localhost:3000",
timeout: 1000,
});
const testCases = [
{
description: "所有字段有效",
data: {
username: "validUser",
password: "validPass123",
email: "valid@example.com",
},
expectedStatus: 200,
expectedMessage: "注册成功",
},
{
description: "用户名太短",
data: {
username: "ab",
password: "validPass123",
email: "valid@example.com",
},
expectedStatus: 400,
},
{
description: "用户名太长",
data: {
username: "a".repeat(31),
password: "validPass123",
email: "valid@example.com",
},
expectedStatus: 400,
},
{
description: "密码不符合正则",
data: {
username: "validUser",
password: "invalid_pass",
email: "valid@example.com",
},
expectedStatus: 400,
},
{
description: "无效的邮箱地址",
data: {
username: "validUser",
password: "validPass123",
email: "invalid-email",
},
expectedStatus: 400,
},
{
description: "缺少必填字段",
data: {
username: "validUser",
password: "validPass123",
},
expectedStatus: 400,
},
{
description: "包含额外字段",
data: {
username: "validUser",
password: "validPass123",
email: "valid@example.com",
extraField: "extra",
},
expectedStatus: 400,
},
];
const runTests = async () => {
for (const testCase of testCases) {
try {
const response = await api.post("/register", testCase.data);
console.log(`${testCase.description}: 成功 - 状态码: ${response.status}`);
if (response.data.message === testCase.expectedMessage) {
console.log(`消息: ${response.data.message}`);
}
} catch (error) {
if (error.response) {
console.log(
`${testCase.description}: 失败 - 状态码: ${error.response.status}`
);
console.log(
`错误信息: ${JSON.stringify(error.response.data.error, null, 2)}`
);
} else {
console.log(`${testCase.description}: 请求失败`);
}
}
}
};
runTests();
7.测试结果(平均值)
两个表格两次不同的测试,第一个可能不够700次
| 测试次数 | Joi (µs) | Ajv (µs) | Zod (µs) | Yup (µs) | Validator.js (µs) | Superstruct (µs) | Class-validator (µs) |
|---|---|---|---|---|---|---|---|
| 700 | 1466.41 | 19388.93 | 442.44 | 1003.02 | 78.12 | 218.92 | 728.75 |
总结
本文详细介绍了七种流行的参数验证库:Joi、Ajv、Zod、Yup、Validator.js、Superstruct和Class-validator,并比较了它们的优势、劣势及适用场景。
- Joi:功能强大,适合复杂验证规则和嵌套对象验证,但体积较大,学习曲线较陡。
- Ajv:性能高,支持JSON Schema标准,适合需要高性能和JSON Schema的场景,但错误信息不够友好。
- Zod:TypeScript优先,类型推断优秀,API简洁,适合TypeScript项目,但功能相对较少。
- Yup:API设计直观,易于使用,适合前端表单验证和简单易用的验证库,但性能稍逊于Ajv。
- Validator.js:轻量级,支持多种常见验证,适合简单字符串验证的场景,但不支持嵌套对象验证。
- Superstruct:轻量级,API简洁,支持嵌套对象验证,但功能相对较少,社区支持较少。
- Class-validator:基于装饰器,适用于TypeScript类,支持复杂验证规则,但仅适用于TypeScript,学习曲线较陡。
通过综合实例展示了如何在HTTP请求中使用这些库进行参数验证,提供了具体的代码示例和测试结果。根据测试结果,各验证库在不同场景下表现出不同的优势和劣势,开发者可以根据项目需求选择合适的验证工具,确保应用程序的安全性、数据完整性和用户体验。