4.3 请求参数解析


4.3 请求参数解析

上一节的矩阵里,订单接口吃进四类输入:路径里的 id、查询串的 status 与 page、请求体里的 items 与 address、请求头里的凭证。这四类东西有一个共同身份——客户端递来的不可信输入。本节先把四类输入的读取位置与信任等级钉清楚,再实现一层 schema 校验中间件,把"参数合法"变成处理器拿到的前置事实。

四类输入:位置、形态与信任等级

输入 读取位置 典型形态 信任等级
路径参数 ctx.params 字符串,恒存在 最低:先验类型再使用
查询串 ctx.query 字符串键值,可重复可缺失 低:全部要校验与转型
请求体 ctx.request.body 解析后的对象(需 bodyparser) 低:结构完全由客户端决定
请求头 ctx.get('name') 字符串,大小写不敏感 分情况:凭证需验证,UA 可参考

三个容易踩的形态细节。其一,query 与 params 一律是字符串ctx.query.page'2' 不是 2,ctx.params.id 也一样,数值化必须显式 parseInt 或走校验层的转型。其二,ctx.query 是解析后的对象,同名键会变成数组(?tag=a&tag=b 得到 ['a','b']),写筛选逻辑时要兼容单值与数组两种形态。其三,请求头用 ctx.get 读取——它是大小写不敏感的规范接口,比直接翻 ctx.headers 对象稳,ctx.get('content-type')ctx.get('Content-Type') 等价。

信任等级从低到高排完,原则就立住了:越靠近客户端的输入越要验。连路径里的 id 也不可信——用户会把 /orders/abc/orders/..%2fadmin 这样的路径发给你,解析成什么全看你的校验。

校验层:把合法性变成前置事实

处理器里散落的 if (!body.items) throw new BadRequest(...) 是坏味道的起点:校验与业务搅在一起,同一规则在五个接口里写五遍。解法是把它抽成一层 schema 驱动的校验中间件,挂在路由声明上(4.1 节的 validate(createSchema) 就是它):

// middleware/validate.js:无依赖的极简实现,思路通用于任何校验库 function validate(schema) { return async (ctx, next) => { const input = { ...ctx.query, ...ctx.params, ...(ctx.request.body || {}) }; const errors = []; for (const [field, rule] of Object.entries(schema)) { const raw = input[field]; const missing = raw === undefined || raw === null || raw === ''; if (rule.required && missing) { errors.push({ field, message: `${field} 为必填` }); continue; } if (missing) continue; // 非必填且缺失:交给默认值逻辑 const value = rule.type === 'number' ? Number(raw) : rule.type === 'array' ? raw : String(raw); if (rule.type === 'number' && !Number.isFinite(value)) { errors.push({ field, message: `${field} 必须是数字` }); continue; } if (rule.type === 'array' && !Array.isArray(value)) { errors.push({ field, message: `${field} 必须是数组` }); continue; } if (rule.max !== undefined && String(raw).length > rule.max) { errors.push({ field, message: `${field} 长度不得超过 ${rule.max}` }); continue; } input[field] = value; // 转型后写回 } if (errors.length) { ctx.status = 400; ctx.body = { code: 'INVALID_ARGUMENT', errors }; // 字段级明细,前端直接映射提示 return; // 截停:不合法的请求不进业务 } ctx.validated = input; // 可信输入挂到 ctx 上 await next(); }; }

业务侧随之变得干净——schema 声明加纯业务处理器:

const createSchema = { items: { type: 'array', required: true }, address: { type: 'string', required: true, max: 200 }, }; router.post('/', auth(), validate(createSchema), async (ctx) => { const { items, address } = ctx.validated; // 类型已保证,直接用 ctx.body = await orderService.create(ctx.state.user.id, items, address); });

验收一把错误路径的输出形态:

curl -i -X POST http://localhost:3000/api/v1/orders \ -H 'authorization: Bearer <token>' -H 'content-type: application/json' -d '{}' # HTTP/1.1 400 Bad Request # {"code":"INVALID_ARGUMENT","errors":[ # {"field":"items","message":"items 为必填"}, # {"field":"address","message":"address 为必填"}]}

一次请求把所有字段错误报全(而不是报一个就返回),前端表单一次就能标完所有红框——这个细节在接口联调阶段能省下大量来回。

该上校验库了吗

手写层适合规则简单、字段不多的项目;表单复杂、嵌套对象、条件规则一多,建议直接上成熟校验库(joi、zjs 风格的 zod、ajv 均有 Koa 集成),中间件的骨架不变——把 schema 判断换成库调用即可。判断标准一句话:当校验规则开始出现嵌套引用与条件分支时,交给库;否则手写层更轻。 两者共同的纪律倒是雷打不动的:校验在路由层与业务层之间、失败回 400 加字段明细、通过后把干净输入挂到 ctx 上。

⚠️ 常见坑:信任请求头里的身份字段。一些老系统直接取 ctx.get('x-user-id') 当登录态用——这个头客户端随手就能伪造。凡是身份类头,只能来自你在可信代理层注入的值(并配合 app.proxy 设置,见 2.1 节),或者由服务端凭证(第 7 章的 JWT)推导,绝不直接信任客户端传来的任何"我是谁"。

处理器里的最后一道防线

校验层挡住了结构与类型,还有一种坏输入要靠业务逻辑兜底:值合法但越界。page=99999、size=100 这类请求,校验层给了合法类型,业务层必须用范围裁剪收口(4.2 节列表接口里 Math.min(100, ...) 那一行就是干这个的)。校验层管"长得对不对",业务层管"合不合理",两层合力,参数这道门才算关严。

💡 关键直觉:参数处理的成熟度,看处理器里 if 校验的数量就行。数量越多,说明校验越没有从业务里剥离出去——最好的处理器读起来像业务说明书,一句校验都没有。

本节要点回顾

  • 四类输入四个位置:params、query、body、headers,读取接口分别是 ctx.params、ctx.query、ctx.request.body、ctx.get;
  • 一律当字符串对待:query 与 params 显式转型,同名 query 键会聚成数组;
  • 校验前置成事实:schema 驱动的校验层挂在路由上,失败回 400 加字段明细,通过挂 ctx.validated;
  • 规则复杂上库:嵌套与条件规则交给成熟校验库,中间件骨架不变;
  • 身份头不可信:x-user-id 类头必须来自可信注入或服务端凭证;
  • 校验管合法性、业务管合理性:范围裁剪是处理器里的最后一道防线。

输入这一关守住了,还有一种特殊的输入没处理——文件。字节流怎么进、怎么落盘、怎么不撑爆内存,下一节讲流式处理的门道。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U