- 后端
【免费下载链接】express-validator
An express.js middleware for validator.js.
导读
本文以 express-validator 6.x 的官方清洗(Sanitization)功能为主线,讲解如何在 Express 请求处理流程中对req.body、req.query、req.params、req.cookies中的输入数据去除噪声(空白、HTML 实体、大小写不一致等),以及如何与校验链(Validation Chain)组合使用。读完本文,你将掌握校验链内嵌清洗、独立清洗中间件(sanitizeBody等)、自定义清洗器(customSanitizer)以及清洗会原地修改请求对象这一关键语义,并能据此写出更健壮、更安全的 Express 接口。
什么是数据清洗:让输入"去掉噪声"
接收 HTTP 请求输入时,我们不仅要确保数据的格式正确,还要确保它没有噪声。用户提交的字段往往带有前后空格、HTML 标签、大小写不一的邮箱地址、字符串形式的布尔值等"噪声"——如果不加处理直接入库、直接拼接进 HTML 页面,轻则数据不整洁,重则引入 XSS 等安全风险。
validator.js 提供了一批现成的清洗函数(sanitizers),express-validator 将这些函数包装成链式 API 和独立中间件,供开发者在 Express 路由中直接调用。
从源码结构看,清洗能力分为两条使用路径:
- 链式清洗:挂在
check、body、query等校验链上(如body('email').isEmail().normalizeEmail()),适用于"既校验又清洗"的字段; - 独立清洗中间件:通过
sanitize、sanitizeBody等函数创建只清洗、不校验的中间件,适用于"不需要校验但需要清洗"的字段。
二者最终都会生成Sanitization上下文项(src/context-items/sanitization.ts),在请求处理时把清洗后的值写回请求对象。
官方示例:在同一链条上完成校验与清洗
官方文档 feature-sanitization.md 给出了一个完整的评论提交接口示例:
const express = require('express'); const { body } = require('express-validator'); const { sanitizeBody } = require('express-validator'); const app = express(); app.use(express.json()); app.post('/comment', [ body('email') .isEmail() .normalizeEmail(), body('text') .not().isEmpty() .trim() .escape(), sanitizeBody('notifyOnReply').toBoolean() ], (req, res) => { // Handle the request somehow });这个示例展示了两个关键点:
email和text字段"既校验又清洗":既然已经对email与text创建了校验链,就可以利用同一条链顺势挂上清洗操作——normalizeEmail()对邮箱做规范化,trim()去除首尾空白,escape()将 HTML 特殊字符转义。- 未校验的字段用独立清洗中间件:
notifyOnReply字段不需要校验,因此使用sanitizeBody函数把它转换为 JavaScript 布尔值。
值得注意,上述代码同时导入了body和sanitizeBody。在 express-validator 6.1.0 中两者均可通过require('express-validator')获取;body是校验链构建器(只作用于req.body),sanitizeBody是清洗中间件构建器(同样只作用于req.body)。
清洗会修改请求对象(重要语义)
Important:请注意,清洗会修改请求(sanitization mutates the request)。
这是使用清洗功能时最容易忽略的一点。官方文档强调:如果req.body.text发送的原始值是" Hello world :>)",清洗之后它的值将变成"Hello world :>)"。
也就是说,清洗原地覆盖了请求对象中的字段值(req.body.text、req.query.x等),而非返回一份清洗后的副本。这一点对后续业务代码有直接影响:
- 路由处理器中读取到的
req.body.text已经是清洗后的值; - 如果同一字段同时参与校验与清洗,校验器看到的是清洗前的原始值(校验链按顺序执行,清洗项排在校验项之后时才会先校验后清洗);
- 因此,不要对同一字段重复清洗,也不要在清洗后还期望拿到原始输入。
从实现上看,src/context-items/sanitization.ts 的run()方法在计算出新值后调用context.setData(path, newValue, location),把新值写回上下文对应的FieldInstance;而 src/context.ts 的setData()直接修改instance.value,随后中间件会把该值同步回请求对象,从而完成"原地修改"。
链式清洗:校验链上可用的全部清洗方法
express-validator 在Sanitizers接口(src/chain/sanitizers.ts)中暴露了以下链式清洗方法,它们可以像校验器一样直接挂在任意校验链后:
| 方法 | 作用 | 参数 |
|---|---|---|
customSanitizer(sanitizer) | 执行自定义清洗函数 | 自定义清洗器 |
default(default_value) | 当值为''、null、undefined或NaN时替换为默认值 | 默认值 |
replace(values_to_replace, new_value) | 用新值替换一个或多个指定值 | 待替换值(数组或单值)、新值 |
blacklist(chars) | 移除字符串中包含于字符集的所有字符 | 字符集字符串 |
escape() | HTML 转义(<、>、&、"、'等) | 无 |
unescape() | 反转义 HTML 实体 | 无 |
ltrim(chars?) | 去除左侧空白(或指定字符) | 可选字符集 |
rtrim(chars?) | 去除右侧空白(或指定字符) | 可选字符集 |
trim(chars?) | 去除两侧空白(或指定字符) | 可选字符集 |
normalizeEmail(options?) | 规范化邮箱(小写、去除点号等,规则由选项控制) | 可选配置对象 |
stripLow(keep_new_lines?) | 移除 ASCII 控制字符 | 可选:是否保留换行符 |
toArray() | 转换为数组(标量包装为单元素数组,空值转为[]) | 无 |
toBoolean(strict?) | 转为布尔值('true'/'1'等) | 可选:是否严格模式 |
toDate() | 转为 Date 对象 | 无 |
toFloat() | 转为浮点数 | 无 |
toInt(radix?) | 转为整数 | 可选进制基数 |
toLowerCase() | 转为小写(仅字符串) | 无 |
toUpperCase() | 转为大写(仅字符串) | 无 |
whitelist(chars) | 仅保留字符集中的字符 | 字符集字符串 |
其中绝大多数方法直接委托给 validator.js 的标准清洗函数,由 src/chain/sanitizers-impl.ts 中的addStandardSanitization()统一包装:
private addStandardSanitization(sanitizer: StandardSanitizer, ...options: any[]) { this.builder.addItem(new Sanitization(sanitizer, false, options)); return this.chain; } blacklist(chars: string) { return this.addStandardSanitization(validator.blacklist, chars); } escape() { return this.addStandardSanitization(validator.escape); }而toLowerCase、toUpperCase、toArray、default、replace等则通过customSanitizer内部的 JavaScript 逻辑实现(见 src/chain/sanitizers-impl.ts)。例如default()的实现是:
default(default_value: any) { return this.customSanitizer(value => [undefined, null, NaN, ''].includes(value) ? _.cloneDeep(default_value) : value, ); }注意default()使用_.cloneDeep复制默认值,测试 src/chain/sanitizers-impl.spec.ts 专门验证了"每个请求得到的是默认对象的独立克隆",避免多个请求共享同一对象引用。
一个更完整的链式清洗示例
const { check, validationResult } = require('express-validator'); app.post('/user', [ // 校验邮箱格式,并把邮箱规范化(小写、去点等) check('email').isEmail().normalizeEmail(), // 用户名去除首尾空白,并仅保留字母数字与下划线 check('username').trim().whitelist('a-zA-Z0-9_'), // 年龄转整数,缺省时默认 18 check('age').toInt().default(18), // 简介转义 HTML,防止 XSS check('bio').escape(), ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } // 此时 req.body.email 已规范化、req.body.age 已是数字 User.create(req.body).then(user => res.json(user)); });独立清洗中间件:sanitize系列函数
对于不需要校验、只需要清洗的字段,express-validator 提供了一组独立的清洗中间件构建函数(官方文档 api-filter.md),均可通过require('express-validator')获取:
| 函数 | 作用 |
|---|---|
sanitize(fields) | 对req.body、req.cookies、req.params、req.query中的字段进行清洗 |
sanitizeBody(fields) | 同sanitize,但仅清洗req.body |
sanitizeCookie(fields) | 同sanitize,但仅清洗req.cookies |
sanitizeParam(fields) | 同sanitize,但仅清洗req.params |
sanitizeQuery(fields) | 同sanitize,但仅清洗req.query |
buildSanitizeFunction(locations) | 自定义清洗位置组合 |
fields可以是单个字段名字符串或字段名数组。这些函数返回一个清洗链(Sanitization Chain),链上可以继续调用上一节列出的全部清洗方法。
sanitize(fields)的注意点
- 字段可能位于
req.body、req.cookies、req.params、req.query中的任意位置; req.headers目前不支持清洗(官方文档明确说明 "not supported at the moment");- 如果同一个字段名在多个位置出现,则所有位置上的该字段值都会被清洗。
buildSanitizeFunction(locations):自定义清洗位置
当默认的五个函数无法满足"组合位置"需求时,可以使用buildSanitizeFunction指定位置数组(body、cookies、params、query中的任意组合)。官方示例:
const { buildSanitizeFunction } = require('express-validator'); const sanitizeBodyAndQuery = buildSanitizeFunction(['body', 'query']); app.put('/update-product', [ // id 无论出现在 req.body 还是 req.query 中,都会被转换为整数 sanitizeBodyAndQuery('id').toInt() ], productUpdateHandler)回到开头的评论接口示例:sanitizeBody('notifyOnReply').toBoolean()仅处理req.body.notifyOnReply,把它从字符串'true'/'false'(或'1'/'0')清洗成真正的 JavaScript 布尔值,供后续业务逻辑直接使用。
底层原理:清洗器是如何被执行并写回的
理解清洗的底层实现,有助于预判边界行为(数组值、非字符串值、异步清洗器等)。
Sanitization上下文项
每个清洗方法最终都会创建一个Sanitization实例(src/context-items/sanitization.ts),构造参数为:清洗函数、是否为自定义清洗器(custom标志)、以及调用参数。
其核心run()逻辑为:
async run(context: Context, value: any, meta: Meta) { const { path, location } = meta; if (this.custom) { const newValue = await runCustomSanitizer(); // 自定义清洗器支持异步 context.setData(path, newValue, location); return; } const values = Array.isArray(value) ? value : [value]; const newValues = values.map(value => { return (this.sanitizer as StandardSanitizer)(this.stringify(value), ...this.options); }); // 若原始值是数组,保留数组结构;否则取数组第一项 context.setData(path, values !== value ? newValues[0] : newValues, location); }由此可以得到几个关键结论:
- 标准清洗器接收字符串:标准清洗函数(如
trim、escape)通过this.stringify(value)把输入转成字符串后再调用,因此即使传入数字等非字符串值也不会报错; - 数组会被逐个清洗:如果字段值是数组,清洗函数会作用在数组的每一个元素上,并保持数组结构返回;
- 自定义清洗器可异步:
customSanitizer可以是返回 Promise 的异步函数(src/chain/sanitizers-impl.spec.ts 中有对应测试),结果同样会被写回字段。
值如何写回请求
Sanitization通过context.setData(path, newValue, location)把新值写入上下文(src/context.ts),如果该字段并未在上下文中预先注册,则会抛出'Attempt to write data that did not pre-exist in context'错误——这解释了为什么清洗链必须建立在有效字段之上。最终上下文运行器(ContextRunner)会把上下文中的数据同步回请求对象的对应位置,完成"清洗修改请求"的效果。
自定义清洗器:customSanitizer的高级用法
内置清洗方法覆盖了大部分场景,但当业务需要特殊规则时,可以使用customSanitizer(sanitizer)注入自定义逻辑。清洗器签名与CustomSanitizer类型一致(src/base.ts):
type CustomSanitizer = (input: any, meta: Meta) => any;其中meta携带req、location、path、pathValues等上下文信息(src/base.ts),让你可以根据请求上下文决定清洗结果。示例:
const { body } = require('express-validator'); app.post('/article', [ // 将 slug 中的连续空白压缩为单个连字符 body('title').customSanitizer(value => String(value).trim().replace(/\s+/g, '-') ), // 根据请求头中的语言设置默认时区 body('timezone').customSanitizer((value, { req }) => value || req.headers['x-timezone'] || 'UTC' ), ]);default()与replace()本质上是内置的customSanitizer便捷封装(见 src/chain/sanitizers-impl.ts),理解了自定义清洗器,也就理解了这两个方法的实现逻辑。
最佳实践小结
- 校验与清洗结合:对需要校验的字段,优先在同一链上完成校验+清洗(如
body('email').isEmail().normalizeEmail()),减少中间件数量; - 只清洗不校验的字段用独立中间件:如
sanitizeBody('notifyOnReply').toBoolean(),避免为清洗单独写校验逻辑; - 牢记清洗会原地修改请求:路由处理器中读取到的就是清洗后的值,不要在清洗后仍假设能拿到原始输入;
- 防 XSS 靠
escape(),防脏数据靠trim()/normalizeEmail():在把用户输入渲染进 HTML 或写入数据库前,务必先完成清洗; - 异步清洗器请用
customSanitizer:它原生支持返回 Promise,适用于需要查库或调用外部服务后决定新值的场景; - 版本注意:本文示例基于 express-validator 6.1.0(文档见 website/versioned_docs/version-6.1.0)。在 6.x 中
sanitizeBody等函数仍可用;迁移到 7.x 时部分 API 有调整,请参考仓库中的 migration-v6-to-v7.md。
延伸阅读
- 校验链中所有可用的校验器与选项:官方文档 docs/api/validator/_validators.md
- 清洗中间件完整参考: api-filter.md
- 自定义校验器与清洗器进阶:feature-custom-validators-sanitizers.md
- Schema 校验中的清洗配置:feature-schema-validation.md
- 相关源码:链式清洗接口 src/chain/sanitizers.ts、实现 src/chain/sanitizers-impl.ts、清洗上下文项 src/context-items/sanitization.ts、测试用例 src/chain/sanitizers-impl.spec.ts
- 后端
【免费下载链接】express-validator
An express.js middleware for validator.js.
相关推荐
express-validator 请求数据清洗(Sanitization)实战:从净化输入到链式清洗
express validator 请求数据清洗(Sanitization)实战:从净化输入到链式清洗 HTTP 请求携带的数据往往既需要校验格式,也需要剔除噪
后端express-validator 数据清洗(Sanitization)实战指南:在验证链中净化请求输入
express validator 数据清洗(Sanitization)实战指南:在验证链中净化请求输入 本文以 express validator v6.13
后端express-validator 数据净化(Sanitization)实战指南:清洗 HTTP 请求输入的完整方案
express validator 数据净化(Sanitization)实战指南:清洗 HTTP 请求输入的完整方案 处理 HTTP 请求输入,很多时候不止要确
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考