☰
express-validator 数据清洗(Sanitization)实战指南:从字段校验到请求净化
2026/10/10 2:46:54 网站建设 项目流程
  • 后端

【免费下载链接】express-validator

An express.js middleware for validator.js.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载

导读

本文以 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 路由中直接调用。

从源码结构看,清洗能力分为两条使用路径:

  1. 链式清洗:挂在check、body、query等校验链上(如body('email').isEmail().normalizeEmail()),适用于"既校验又清洗"的字段;
  2. 独立清洗中间件:通过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); }

由此可以得到几个关键结论:

  1. 标准清洗器接收字符串:标准清洗函数(如trim、escape)通过this.stringify(value)把输入转成字符串后再调用,因此即使传入数字等非字符串值也不会报错;
  2. 数组会被逐个清洗:如果字段值是数组,清洗函数会作用在数组的每一个元素上,并保持数组结构返回;
  3. 自定义清洗器可异步: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),理解了自定义清洗器,也就理解了这两个方法的实现逻辑。

最佳实践小结

  1. 校验与清洗结合:对需要校验的字段,优先在同一链上完成校验+清洗(如body('email').isEmail().normalizeEmail()),减少中间件数量;
  2. 只清洗不校验的字段用独立中间件:如sanitizeBody('notifyOnReply').toBoolean(),避免为清洗单独写校验逻辑;
  3. 牢记清洗会原地修改请求:路由处理器中读取到的就是清洗后的值,不要在清洗后仍假设能拿到原始输入;
  4. 防 XSS 靠escape(),防脏数据靠trim()/normalizeEmail():在把用户输入渲染进 HTML 或写入数据库前,务必先完成清洗;
  5. 异步清洗器请用customSanitizer:它原生支持返回 Promise,适用于需要查库或调用外部服务后决定新值的场景;
  6. 版本注意:本文示例基于 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.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载
上一篇:从200%到24%耗时:Spirula Studio 3D高斯泼溅共享内存优化指南
下一篇:如何快速看懂 CodexManager 网关与 Codex 官方请求对齐:请求头与参数差异表深度剖析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询