- 后端
【免费下载链接】express-validator
An express.js middleware for validator.js.
checkExact()是 express-validator 提供的一种"反向"校验中间件:传统校验只关心"声明过的字段是否符合规则",而它负责检查请求中是否出现了没有对应校验链(validation chain)的未知字段,一旦发现即以unknown_fields错误类型上报。本文基于仓库中的官方文档 check-exact.md 并结合 src/middlewares/exact.ts 源码,完整讲解其签名、known fields 语义、locations选项、与checkSchema()的配合、手动运行方式以及底层的未知字段探测算法,读完即可在自己的 express.js 路由中落地"白名单式"的精确字段校验。
checkExact()是什么:签名与核心语义
checkExact()的完整类型签名如下:
checkExact( chains?: ValidationChain | ValidationChain[] | (ValidationChain | ValidationChain[])[], options?: { locations?: Location[], message?: any } ): Middleware & ContextRunner它返回的对象同时是两种角色的复合体:
- 一个 express.js中间件(
Middleware),可直接挂载到路由上; - 一个实现了
ContextRunner接口的运行器,可以手动调用.run(req)获取校验结果。
"已知字段"(known fields)的判定规则
checkExact()会检查请求中是否恰好只包含那些拥有与之关联的 validation chain 的字段。文档中明确了核心语义:
一个字段被
checkExact()视为"已知",当且仅当在checkExact()运行之前,已经为它指定过校验链。
这句话包含两个关键限定:
- 时间维度:只有先于
checkExact()执行过的校验链才会计入"已知"集合。这是本 API 最容易踩坑的地方(详见下文"重要警告"一节)。 - 空间维度:若把校验链作为参数传给
checkExact(),那么这些链会在checkExact()运行时一并执行,且它们所瞄准的字段会被额外计入"已知"集合——即"已知字段 = 之前声明的链 + 本次传入的链"。
从源码看,src/middlewares/exact.ts 的run函数正是这么实现的:先通过runAllChains(req, chainsArr)运行传入的链,再从请求对象上挂载的contextsKey(express-validator#contexts,见 src/base.ts)读取此前所有链注册的 context,把每个 context 的fields按locations聚合进fieldsByLocation映射表,形成"已知字段清单"。
一个完整的中间件用法
以下示例中,name、email、password都是req.body中的已知字段,请求中出现的任何其他字段都会被判定为未知:
app.post( '/signup', // All of name, email and password are known fields in req.body. Everything else is unknown. body('name').notEmpty(), checkExact([body('email').isEmail(), body('password').isLength({ min: 8 })], { message: 'Too many fields specified', }), (req, res) => { // Handle request }, );注意这里email和password的校验链是作为参数传入checkExact()的,因此它们既会被正常执行(isEmail()、isLength()照常生效),其目标字段又会被自动视为已知。
发现未知字段时会发生什么
如果请求中存在未知字段,checkExact()会向请求添加一个类型为unknown_fields的UnknownFieldsError错误,默认消息为Unknown field(s)。其结构定义于 src/base.ts:
type UnknownFieldsError = { type: 'unknown_fields'; msg: any; fields: { path: string; location: Location; value: any }[]; };fields列出了所有未知字段的路径(path)、所在位置(location)与实际值(value);- 该错误会与其他校验错误一样被
validationResult(req)收集,可以配合.array()、.mapped()等方法统一处理(参见 validation-result.md); - 默认消息可通过
options.message覆盖,若传的是函数,则必须是UnknownFieldMessageFactory类型(详见下文)。
重要警告:checkExact()必须是最后一个验证中间件
官方文档用:::caution强调了一个极易踩中的陷阱:
checkExact()必须是请求中最后一个运行的验证中间件,否则它会把后续才拥有校验链的字段误判为未知字段。
原因正是"已知字段"判定的时间维度:checkExact()只会把在它之前注册过的 context 视为已知。下面这个反例中,email是已知字段,但subscribe不是——因为它的校验链排在checkExact()之后:
app.post( '/newsletter/subscription', body('email').isEmail(), checkExact(), // 此时 subscribe 还没有校验链,会被当作未知字段 body('subscribe').isBoolean(), (req, res) => { // Handle request }, );当请求中带了subscribe字段时,checkExact()会生成unknown_fields错误,尽管业务上你确实打算校验它。从源码可以印证这一行为:run函数遍历internalReq[contextsKey](即"已经挂到请求上的 context 列表")来收集已知字段,checkExact()之后才运行的链,其 context 自然不在其中。
因此实践中的铁律是:把所有字段校验链放在前面,checkExact()永远放最后。若用body()/query()等链构建器声明字段,请确保它们全部位于checkExact()之前或作为参数传入。
locations选项:控制检查范围
checkExact()默认只检查三个请求位置(location),可通过options.locations自定义:
默认值仅包含
body、params和query。options.locations范围之外的未知字段不会被报告。
type Location = 'body' | 'cookies' | 'headers' | 'params' | 'query';源码 src/middlewares/exact.ts 中的默认值为opts?.locations || ['body', 'params', 'query'],注释也点明了设计意图:默认不检查cookies和headers,以避免把用户正常请求判为非法。官方文档给出了三个理由:
- HTTP 规范定义了大量 headers,用途与产生原因各异。你不可能为了通过
checkExact()而把所有这些 header 全部声明为已知字段,否则会带来大量误报(false negatives 的反面——合法请求被拒绝); - 浏览器和代理会不断引入新 header,需要"已知"的 header 清单会无限膨胀,让
checkExact()在 header 上难以稳定工作; - 浏览器默认会自动携带 cookies随每个请求发送,而页面上的 JS 广告或分析脚本通常会在宿主站点创建 cookie,这会让针对 cookies 的严格检查非常恼人。
基于以上原因,req.cookies与req.headers默认不参与未知字段检查;如果你确实需要严格管控它们,再通过locations显式加入。
四类实战示例
官方文档提供了四个典型场景的示例,覆盖了checkExact()的主要使用形态。
示例一:不带输入校验链
如果路由中的字段已经由前面的链声明过,可以直接传空数组:
app.post( '/signup', body('email').isEmail(), body('password').isLength({ min: 8 }), checkExact([], { message: 'Only email and password are allowed' }), (req, res) => { // Handle request }, );此时checkExact()会把email、password视为已知(因为前面的链已经注册了 context),req.body、req.query、req.params中的任何其他字段都会触发UnknownFieldValidationError,错误消息被自定义为Only email and password are allowed。
示例二:与checkSchema()组合
checkSchema()返回的是一个校验链列表,因此可以直接作为chains参数传给checkExact():
app.post( '/signup', checkExact( checkSchema({ email: { isEmail: true }, password: { isLength: { options: { min: 8 } } }, }), ), (req, res) => { // Handle request }, );这一组合非常契合"白名单式"接口设计:schema 同时声明了字段的校验规则和"允许出现"的边界,且源码中的CheckExactInput类型(src/middlewares/exact.ts)允许传入单个链、链数组或"链与链数组混合的数组",checkSchema()返回的链列表正好属于第二种。
示例三:只检查req.body
若你只关心 body 中的未知字段,其他位置(如 query、params)允许自由出现额外字段:
app.post( '/signup', body('email').isEmail(), body('password').isLength({ min: 8 }), checkExact([], { locations: ['body'] }), (req, res) => { // Handle request }, );设置locations: ['body']后,检查范围被收窄到req.body,req.query、req.params等位置的未知字段将不会被报告。测试用例 src/middlewares/exact.spec.ts 也验证了这一点:当设置locations: ['headers']时,只会从req.headers中发现未知字段。
示例四:手动运行(ContextRunner 模式)
checkExact()返回的中间件天然适合挂载到 express.js 路由,但它同时也实现了ContextRunner接口,可以像校验链一样手动运行:
app.post( '/signup', body('email').isEmail(), body('password').isLength({ min: 8 }), async (req, res) => { const result = await checkExact().run(req); if (result.isEmpty()) { console.log('No unknown fields in the request'); } }, );ContextRunner的接口定义为run(req, options?): Promise<Result>(见 src/chain/context-runner.ts),返回的Result可用.isEmpty()、.array()等方法判断。从源码看,checkExact()通过Object.assign(middleware, { run })把run函数同时挂到中间件对象上(src/middlewares/exact.ts),这正是"既是中间件、又能手动运行"的机制来源。更完整的自定义运行器模式可参考 manually-running.md。
UnknownFieldMessageFactory:定制未知字段错误消息
当options.message传的是函数时,它必须符合UnknownFieldMessageFactory类型:
type UnknownFieldMessageFactory = ( unknownFields: UnknownFieldInstance[], opts: { req: Request }, ) => any;该类型定义于 src/base.ts,接收两个参数:
unknownFields:本次发现的所有未知字段实例列表,每个实例包含path(字段路径)、location(所在位置)与value(字段值);opts:包含当前req的上下文对象。
典型用法如下,可以按字段维度生成更具体的错误信息:
checkExact([body('name').notEmpty(), body('email').isEmail()], { message: fields => { const [field] = fields; return `Unknown field ${field.path} in ${field.location} with value ${field.value}`; }, });在 src/context.ts 的addError中可以看到,unknown_fields类型错误的消息正是通过typeof msg === 'function' ? msg(opts.fields, { req: opts.req }) : msg来解析的——传入函数时会以fields和{ req }为参数调用,返回的值会成为msg。
底层原理:未知字段是如何被探测出来的
checkExact()的核心探测逻辑不在exact.ts本身,而在 src/field-selection.ts 的selectUnknownFields函数。
已知字段的树形建模
算法先把所有已知字段路径构建成一棵前缀树(Tree),例如已知字段foo.bar会生成{ foo: { bar: { '': {} } } }这样的嵌套结构。随后对每个受检位置的请求数据做深度优先搜索(findUnknownFields,src/field-selection.ts),凡是树中没有任何分支覆盖的键即被判定为未知。
该算法对以下三种"覆盖"情形做了特殊处理,因此表现与直觉一致:
- 整枝校验:若某个分支以空字符串键结尾(如
{ foo: { '': {} } },即foo被整体校验),则其下的所有子字段都不算未知; - 路径单独校验:
foo.bar被单独校验过,则该路径本身不算未知; - 通配符覆盖:
*(单层通配)与**(globstar,多层通配)覆盖到的路径均视为已知。
关键边界行为
从findUnknownFields的源码还可以推断出几个值得注意的边界行为:
- 非对象请求体:如果
req.body是字符串等原始值且没有被任何链校验(treePath为空且无 globstar 分支),整个请求体会被当作未知字段上报; - 通配符残留:
foo.**.bar这类 globstar 路径只会覆盖匹配的叶子,未命中的叶子仍会被标记为未知; - 多分支合并去重:当多个分支(如
foo与foo.*.bar)同时存在时,更全面的分支(foo)会使较窄分支的"未知"结论被抑制,避免重复或误报; - 路径重建:未知字段的
path由reconstructFieldPath(src/field-selection.ts)重建,数字段会被包装成数组下标语法(如foo[0].bar),包含特殊字符(如.)的键会被包装成foo["bar.baz"].qux,与 JS 对象访问语法保持一致。
传入链的执行顺序
checkExact()内部通过 src/utils.ts 的runAllChains运行传入的链。该函数对带请求级bail的链做了顺序处理:遇到 bailed 链时会先等待之前的链全部完成,若该链结果非空则中断后续链的执行,从而保证错误聚合结果不被扭曲。
测试验证:源码如何保证行为正确
仓库中的 src/middlewares/exact.spec.ts 为上述行为提供了完整的测试覆盖,可以作为理解checkExact()语义的"活的文档":
- 不同链参数形态:单条链、链数组、"链与链数组混合数组"都能正确发现未知字段,并只生成一个
unknown_fields错误(spec 第 5-25 行); - 继承先前链:先手动运行
check('banana').run(req),再运行checkExact(),banana会被视为已知,而apple被报为未知(spec 第 27-38 行)——这正是"已知字段时间维度"的实证; - 默认位置:默认只检查
body、params、query,而cookies、headers中的字段即使存在也不会被报告(spec 第 54-73 行); - 零误报:所有字段都有对应链时,
result.isEmpty()为true,不会生成任何错误(spec 第 75-82 行); - 中间件形态:
checkExact([])可直接作为中间件调用并正确进入next()(spec 第 84-89 行)。
小结与推荐实践
checkExact()为 express-validator 补齐了"请求字段白名单"这一环,与传统的"字段规则校验"形成互补。综合文档与源码,推荐的落地姿势是:
- 位置放在最后:确保
checkExact()是路由中最后一个验证中间件,或把其余字段链作为参数传入; - 默认位置即可:大多数场景下保持默认的
body、params、query检查范围,避免对 headers/cookies 误伤; - 配合
checkSchema():用 schema 同时声明字段规则与允许边界,代码最简洁; - 自定义错误消息:通过
UnknownFieldMessageFactory生成包含path、location、value的详细错误,便于客户端定位; - 必要时手动运行:利用
ContextRunner接口在自定义中间件中灵活控制执行时机与结果处理。
更完整的内容可继续阅读同一版本文档中的 check.md、validation-result.md 与 misc.md,以及源码 src/middlewares/exact.ts 和 src/field-selection.ts 中的实现细节。
- 后端
【免费下载链接】express-validator
An express.js middleware for validator.js.
相关推荐
express-validator字段选择指南:精准定位请求数据
express validator字段选择指南:精准定位请求数据 引言 在Web开发中,处理客户端提交的数据是核心任务之一。express validator作
后端Express-Validator 字段选择指南:精准定位请求数据
Express Validator 字段选择指南:精准定位请求数据 引言 在Web开发中,处理用户输入数据是至关重要的一环。Express Validator作
后端Kubernetes 排障实战指南:基于 claude-skills 技能的 kubectl 调试命令、故障根因与诊断工具全解
Kubernetes 排障实战指南:基于 claude skills 技能的 kubectl 调试命令、故障根因与诊断工具全解 本文是 claude skill
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考