- 后端
【免费下载链接】express-validator
An express.js middleware for validator.js.
express-validator 的 Sanitization Chain(净化链)是一种可挂载到 Express 路由上的中间件,用于对请求字段(如req.body、req.query、req.params)进行原地(in-place)清洗。本指南将带你完整掌握 v5.3.0 中净化链的创建方式、内置 sanitizer 清单、.customSanitizer()自定义清洗器,以及其背后"按序应用、原地写回"的源码级执行原理,让你能在生产代码中直接写出可复用的数据清洗中间件。
净化链的本质:一个会原地改写请求的中间件
在 express-validator 5.3.0 中,净化链(Sanitization Chain)首先是一个 Express 中间件,它应当被传入路由处理器(route handler)中。当该中间件运行时,它会按"声明顺序"(the order they were specified)逐一应用链上的每个 sanitizer,并**就地修改(modify in place)**每个匹配到的字段——也就是说,清洗后的新值会直接写回请求对象,后续的处理器拿到的是已经净化过的数据。
官方文档给出了最直观的例子:
app.get('/', sanitizeBody('trimMe').trim(), (req, res, next) => { // 如果 req.body.trimMe 原本是 " something ", // 经过清洗后其值将是 "something" console.log(req.body.trimMe); });在这个例子中:
sanitizeBody('trimMe')创建了一个针对req.body.trimMe字段的净化链;.trim()是 validator.js 内置的清洗方法,按链上顺序被执行;- 中间件执行完毕后,
req.body.trimMe已被原地改写为"something",因此路由处理器里读取到的就是清洗后的值。
关于sanitizeBody等创建函数的出处:在 v5.3.0 中,它们来自express-validator/filter模块,即所谓的 Filter API,参见 api-filter.md;完整的用法示例可见 feature-sanitization.md。
validator.js 内置 sanitizer 的全面继承
文档明确说明:只要 express-validator 支持的是最新版本的 validator.js,那么 validator.js 列出的所有 sanitization 方法都会在 express-validator 创建的所有净化链上可用。
从当前仓库的实际依赖来看,package.json 中声明了"validator": "~13.15.35",也就是说净化链能够直接调用 validator.js 13.x 提供的全部清洗器。
在源码 src/chain/sanitizers.ts 的Sanitizers接口中,可以看到 express-validator 显式声明并转发的这一批内置清洗方法:
| 方法 | 签名 | 说明(结合源码 src/chain/sanitizers-impl.ts 实现) |
|---|---|---|
blacklist(chars) | 必填chars: string | 删除字符串中出现在chars黑名单里的字符 |
escape() | 无参 | HTML 转义,如&、<、>转义为实体 |
unescape() | 无参 | escape()的逆操作,还原 HTML 实体 |
ltrim(chars?) | 可选chars: string | 去除字符串左侧空白(或指定的chars) |
normalizeEmail(options?) | 可选options: NormalizeEmailOptions | 规范化邮箱,如小写、去除点号等 |
rtrim(chars?) | 可选chars: string | 去除字符串右侧空白(或指定的chars) |
stripLow(keep_new_lines?) | 可选keep_new_lines: boolean | 移除 ASCII 控制字符,可保留换行符 |
toArray() | 无参 | 将非数组值包装为单元素数组 |
toBoolean(strict?) | 可选strict: boolean | 转换为布尔值;strict为true时仅接受'true'/'1'等严格语义 |
toDate() | 无参 | 尝试转换为 Date |
toFloat() | 无参 | 转换为浮点数 |
toInt(radix?) | 可选radix: number | 转换为整数,可指定进制 |
toLowerCase() | 无参 | 字符串转小写 |
toUpperCase() | 无参 | 字符串转大写 |
trim(chars?) | 可选chars: string | 去除字符串两端空白(或指定的chars) |
whitelist(chars) | 必填chars: string | 仅保留白名单chars中的字符 |
这些方法从源码结构上看可以分为两类:
- 标准清洗器(Standard Sanitizer):直接转发给 validator.js 的对应函数,如
trim、escape、toInt等,通过addStandardSanitization(sanitizer, ...options)统一注册; - 自定义清洗器(Custom Sanitizer):
toArray、toLowerCase、toUpperCase其实是基于customSanitizer在库内部实现的小工具函数,例如:
toLowerCase() { return this.customSanitizer(value => (typeof value === 'string' ? value.toLowerCase() : value)); }也就是说,toLowerCase并不会把值交给 validator.js,而是由 express-validator 内部用自定义清洗器实现,并且只对字符串类型生效,其他类型原样返回(参见 src/chain/sanitizers-impl.ts)。
除了内置方法外,净化链还额外提供.customSanitizer()
除了 validator.js 的全部清洗器,v5.3.0 的净化链还额外提供一个扩展方法——这正是本篇文档的核心 API。
.customSanitizer(sanitizer)
- 参数
sanitizer(value, { req, location, path }):自定义清洗器函数,接收被清洗字段的当前值,以及一个包含req(express 请求对象)、location(字段所在位置,如body/query/params)和path(字段路径)的元信息对象。 - 返回值:当前净化链实例(便于继续链式调用)。
- 行为:向当前净化链添加一个自定义清洗器。它必须同步地返回新值(synchronously return the new value),这一点与自定义验证器支持 Promise 异步不同。
官方示例:
app.get('/object/:id', sanitizeParam('id').customSanitizer((value, { req }) => { return req.query.type === 'user' ? ObjectId(value) : Number(value); }), objectHandler)该示例展示了一个非常典型的按请求上下文定制清洗的场景:当req.query.type为'user'时,把路由参数id清洗为 MongoDB 的ObjectId;否则转换为Number。由于req被显式地传入了清洗器,你可以根据请求的其他部分动态决定清洗策略——这是内置 sanitizer 无法直接做到的。
从实现上看(src/chain/sanitizers-impl.ts),customSanitizer做的事情非常直接:
customSanitizer(sanitizer: CustomSanitizer) { this.builder.addItem(new Sanitization(sanitizer, true)); return this.chain; }即:把清洗函数包装成一个Sanitization上下文项(custom标记为true),加入当前链的上下文构建器中,然后返回链实例。测试 src/chain/sanitizers-impl.spec.ts 也验证了这一点:调用customSanitizer后会通过builder.addItem追加一个标记为 custom 的Sanitization项。
底层原理:清洗器是如何"按序 + 原地"执行的
要理解"按声明顺序应用并原地修改字段",需要看两层源码。
第一层:Sanitization上下文项如何计算新值
每个 sanitizer 最终都会变成 src/context-items/sanitization.ts 中的一个Sanitization上下文项。它的run()方法区分两种路径:
- 自定义清洗器(
custom === true):直接调用this.sanitizer(value, meta),把返回值作为新值; - 标准清洗器:先把值用
toStringImpl转成字符串,再调用this.sanitizer(stringifiedValue, ...options);如果原值是数组,则对数组逐项清洗;若原值是被包装成数组的标量,则只取清洗结果的第一个元素(参见 src/context-items/sanitization.ts)。
无论哪条路径,最终都会调用context.setData(path, newValue, location)把新值写回上下文的字段实例中。
第二层:ContextRunnerImpl如何把新值写回req
src/chain/context-runner-impl.ts 中的ContextRunnerImpl.run()负责驱动整个中间件执行:
- 通过
selectFields按字段与 location 从请求中选出所有匹配的字段实例; - 按
context.stack(即链上依次添加的所有上下文项)逐项串行处理; - 每个字段实例的每一项执行完后,比较
req[location]中的旧值与清洗后的新值是否不同; - 若不同,则通过
_.set(req[location], path, newValue)(路径为空时_.set(req, location, newValue))将新值原地写回请求对象(参见 src/chain/context-runner-impl.ts)。
这正是"中间件会原地修改每个字段"这一行为在源码层面的完整证据链。附带一提,run()支持dryRun选项({ dryRun: true }),此时只计算清洗结果而不写回请求,适合做预演验证。
实战:在同一中间件中"先校验后清洗"
净化链并不一定单独存在——v5.3.0 的验证链(Validation Chain)本身也支持调用 sanitizer,且被验证的值是清洗后的值(官方文档 api-validation-chain.md 明确说明:"If you use any of the sanitizers together with validators, the validated value is the sanitized one.")。
官方指南 feature-sanitization.md 给出了完整的组合示例:
const express = require('express'); const { body } = require('express-validator/check'); const { sanitizeBody } = require('express-validator/filter'); 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 });该示例演示了两种典型用法:
- 在验证链上就地清洗:
body('email').isEmail().normalizeEmail()先校验邮箱合法性,再规范化邮箱;body('text').not().isEmpty().trim().escape()校验非空后再去空白、转义 HTML。注意此时验证发生在清洗之后,即校验的就是清洗后的值。 - 对未验证字段单独清洗:
notifyOnReply不需要校验,直接用 Filter API 的sanitizeBody(...).toBoolean()把它转换为 JS 布尔值。
关于原地修改的重要提醒
官方文档特别强调:清洗会改动(mutate)请求本身。例如若req.body.text被发送为Hello world :>),经过清洗后其值会变成Hello world :>)。也就是说,后续路由处理器、日志、以及任何依赖原始值的业务逻辑,看到的都是已被改写的数据。如果业务上需要保留原始输入,请在清洗前自行备份。
小结与使用建议
围绕 v5.3.0 的 api-sanitization-chain.md,本文系统梳理了净化链的完整知识:
- 中间件语义:净化链是传给 Express 路由的中间件,运行时会按声明顺序应用 sanitizer 并原地写回字段;
- 方法全集:validator.js 13.x(见 package.json)的全部清洗器 + 库内基于
customSanitizer实现的toArray/toLowerCase/toUpperCase,均可在净化链上调用(见 src/chain/sanitizers.ts); - 自定义清洗:
.customSanitizer(sanitizer)接收(value, { req, location, path }),必须同步返回新值,且返回当前链以便继续链式调用; - 执行原理:
Sanitization上下文项计算新值 →ContextRunnerImpl将新值写回req(见 src/context-items/sanitization.ts 与 src/chain/context-runner-impl.ts)。
最后给出三条实战建议:
- 清洗顺序即执行顺序:链上的方法从左到右依次执行,把"粗清洗"(如
trim)放在前面、"类型转换"(如toInt、toBoolean)放在后面,避免转换后的值再次被字符串化处理; - 需要上下文时优先用
customSanitizer:凡是清洗策略依赖请求其他字段(如 URL 类型、用户角色、租户配置)的场景,都适合在customSanitizer中通过req参数动态决策; - 牢记原地可变性:清洗会直接改写请求对象,若需保留原始值,务必先复制。这一行为在 v5 之后的主线版本中依然延续,理解本版的执行模型有助于阅读后续版本的演进。
- 后端
【免费下载链接】express-validator
An express.js middleware for validator.js.
相关推荐
express-validator 清洗链(Sanitization Chain)完整指南:中间件用法、标准清洗器与自定义清洗器
express validator 清洗链(Sanitization Chain)完整指南:中间件用法、标准清洗器与自定义清洗器 本篇指南围绕 express
后端PDF补丁丁 PDF编辑教程:书签、合并拆分、批量处理四大场景实操
PDF补丁丁 PDF编辑教程:书签、合并拆分、批量处理四大场景实操 周五下午,你拿到十几个待整理的PDF:有的没有书签,有的页面尺寸不一,有的要合并成一个文件。
后端express-validator 请求数据清洗(Sanitization)实战:从净化输入到链式清洗
express validator 请求数据清洗(Sanitization)实战:从净化输入到链式清洗 HTTP 请求携带的数据往往既需要校验格式,也需要剔除噪
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考