Node系列 · ORM:数据验证
数据验证是后端的"第一道防线"——不合法数据应该在入库前被拦下。本章讲清楚字符串级验证(
validator)、对象级 schema 验证(validator.js/ Joi)、以及验证的层次划分。
一、为什么要验证
| 风险 | 验证可解决的问题 |
|---|---|
| 数据脏 | 邮箱格式错、手机号位数错,导致后续短信发不出去 |
| 安全 | XSS(name = '<script>...')、SQL 注入(name = "1' OR ...") |
| 业务错 | 数量为负、起止日期倒置 |
| 崩溃 | 字段长度超出数据库限制,写入报错 |
DANGER
永远不要信任客户端传来的数据。前端表单验证只是 UX,后端验证才是安全底线——绕过前端是分分钟的事。
二、字符串级验证:validator
最常用的字符串验证库。
bash
npm install validator2.1 常用验证方法
javascript
const validator = require('validator');
// 邮箱
validator.isEmail('alice@example.com'); // true
validator.isEmail('not-an-email'); // false
// 手机号(不同国家规则不同)
validator.isMobilePhone('13800138000', 'zh-CN'); // true
validator.isMobilePhone('13800138000', 'en-US'); // false
// URL
validator.isURL('https://example.com'); // true
// IP
validator.isIP('192.168.1.1'); // true
validator.isIP('192.168.1.1', 'ipv4'); // 显式指定
validator.isIP('::1', 'ipv6'); // true
// 邮箱(更严格)
validator.isEmail('a@b', { allow_display_name: false });
// 是否数字
validator.isNumeric('123'); // true
validator.isInt('42'); // true
validator.isFloat('3.14'); // true
// 是否 URL slug / UUID
validator.isSlug('hello-world'); // true
validator.isUUID('550e8400-e29b-41d4-a716-446655440000'); // true2.2 字符串清洗
javascript
// 转义 HTML(防 XSS)
validator.escape('<script>alert("xss")</script>');
// '<script>alert("xss")</script>'
// 去除首尾空白
validator.trim(' hello '); // 'hello'
// 规范化邮箱(小写)
validator.normalizeEmail('Alice@Example.COM');
// 'alice@example.com'
// 去除 ASCII 控制字符(0x00-0x1F)
const dirty = "hello�world"; // 含 BEL(0x07) 与 NUL(0x00)
const clean = validator.stripLow(dirty);
// 'helloworld'```
### 2.3 自定义错误信息
```javascript:validator-errors.js
if (!validator.isEmail(email)) {
throw new Error('邮箱格式不正确');
}
if (!validator.isMobilePhone(phone, 'zh-CN')) {
throw new Error('手机号必须是 11 位数字');
}三、对象级验证:validator.js
对复杂对象做 schema 验证。validator.js(npm 包名)不是 validator(字符串验证库)——注意区分。
bash
npm install validatorjs3.1 基本用法
javascript
const Validator = require('validatorjs');
const data = {
name: 'Alice',
email: 'alice@example.com',
age: 25,
};
const rules = {
name: 'required|string|min:2|max:50',
email: 'required|email',
age: 'required|integer|min:0|max:150',
};
const validation = new Validator(data, rules);
if (validation.fails()) {
console.log(validation.errors.all());
// {
// email: ['The email format is invalid.'],
// age: ['The age must be an integer.']
// }
} else {
console.log('验证通过');
}3.2 常用规则
| 规则 | 含义 |
|---|---|
required | 必填 |
string / integer / numeric / boolean | 类型 |
email / url / uuid | 格式 |
min:n / max:n | 最小/最大 |
size:n | 长度等于 n |
in:a,b,c | 在枚举中 |
regex:/pattern/ | 正则 |
same:field | 与某字段相等 |
different:field | 与某字段不等 |
confirmed | 与 xxx_confirmation 字段相等(密码二次确认) |
3.3 自定义规则
javascript
Validator.register('strong_password', (value) => {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(value);
}, '密码必须 8 位以上,含大小写字母和数字');
// 使用
const rules = { password: 'required|string|strong_password' };3.4 异步验证
javascript
Validator.registerAsync('unique_email', async (value, attribute, req, passes) => {
const exists = await User.findOne({ where: { email: value } });
if (exists) {
return passes(false, '邮箱已被注册');
}
return passes();
}, '邮箱唯一性校验');
// 使用
const validation = new Validator(data, {
email: 'required|email|unique_email',
});
validation.checkAsync(() => {
if (validation.fails()) {
return res.status(400).json({ errors: validation.errors.all() });
}
// 继续处理
});四、其他验证库对比
| 库 | 风格 | 大小 | 异步 | 推荐 |
|---|---|---|---|---|
| validator | 函数式字符串验证 | 很小 | ❌ | ✅ 基础工具 |
| validatorjs | schema 字符串 | 中 | ✅ | ✅ 中等项目 |
| Joi | 链式 schema | 大 | ✅ | ✅ 大型项目 |
| Yup | schema(类 Joi) | 中 | ✅ | ✅ 前后端共享 |
| Zod | TS schema | 中 | ✅ | ✅ TS 项目首选 |
| class-validator | 装饰器(TS) | 中 | ✅ | NestJS 项目 |
| ajv | JSON Schema | 中 | ✅ | 需要 JSON Schema 兼容 |
4.1 Joi 示例
javascript
const Joi = require('joi');
const schema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150).required(),
});
const { error, value } = schema.validate(req.body);
if (error) return res.status(400).json({ error: error.details });
// value 是类型安全的(TS 推断)4.2 Zod 示例(TS 项目)
typescript
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(0).max(150),
});
type User = z.infer<typeof UserSchema>; // 自动推导 TS 类型
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const user: User = result.data; // 类型安全五、验证的层次
数据验证应该在多层进行,每层有不同的关注点:
| 层次 | 关注 | 工具 |
|---|---|---|
| 前端表单 | 必填、长度、即时格式提示 | HTML5 validation / 表单库 |
| API 接收 | 类型、格式、字段约束 | Joi / Zod / validatorjs |
| 数据库 | 唯一键、外键、CHECK | MySQL 约束 |
| 业务逻辑 | 跨字段、跨表、异步唯一性 | 应用代码 |
TIP
不要把验证只放在前端——绕过前端是分分钟的事。也不要把跨表逻辑放在数据库约束里(性能差、维护难)。合理的层次是:
- 前端:UX 提示,不安全
- 后端 API:硬验证,必须
- 数据库:兜底约束(避免最坏情况)
- 业务逻辑:跨字段、异步规则
六、与 ORM 集成
Sequelize 的 validate 只做字段级校验,不支持跨字段。要更复杂校验用 validatorjs / Joi:
javascript
const Validator = require('validatorjs');
const { User } = require('./models');
// Express 中间件风格
function validateUser(req, res, next) {
const rules = {
name: 'required|string|min:2|max:50',
email: 'required|email',
password: 'required|string|min:8',
passwordConfirm: 'required|same:password',
};
const validation = new Validator(req.body, rules);
if (validation.fails()) {
return res.status(400).json({ errors: validation.errors.all() });
}
next();
}
app.post('/api/users', validateUser, async (req, res) => {
// 验证通过,处理业务
});七、最佳实践
| 场景 | 推荐 |
|---|---|
| 字符串验证 | validator(isEmail / isMobilePhone / isURL) |
| 对象验证 | Joi / Zod(TS 项目) |
| XSS 防护 | 输入时 validator.escape,输出时模板默认转义 |
| 唯一性 | 异步验证(查数据库),不要在同步 schema 里做 |
| 错误响应 | 返回 { field: [msg, ...] } 结构,前端按字段提示 |
| 验证位置 | API 层必须有;数据库层做兜底 |
| 时机 | 越早越好;在控制器入口验证,不要到 service 层才检查 |
八、小结
- 字符串验证:
validator.isEmail/isMobilePhone/isURL/escape - 对象验证:
validatorjs(轻量)/Joi(强大)/Zod(TS 首选) - 验证分四层:前端 UX / API 必填 / 数据库兜底 / 业务逻辑
- 永远不在前端验证作为安全防线——后端 API 必须做硬验证
- 异步唯一性用
Validator.registerAsync+ 数据库查询 - XSS 用
validator.escape输入转义;输出靠框架默认转义
