参数验证实战:提升API安全与数据可靠性的关键技巧

a'ゞ萍萍 安全 阅读 2,259
赞 23 收藏
二维码
手机扫码查看
反馈

为什么我又在折腾参数验证?

最近重构一个老项目,发现接口里传的参数五花八门,有的字段明明是数字,前端传了个字符串;有的必填字段直接没传,后端也没拦住,结果数据库里一堆 null。我一查日志,好家伙,光上个月就因为参数问题导致了 3 次线上异常。所以这次我决定把参数验证这块彻底理清楚——不是简单加个 if 判断就完事,而是选一套靠谱、可维护、不拖累开发效率的方案。

参数验证实战:提升API安全与数据可靠性的关键技巧

我试过几种主流做法:纯手写校验逻辑、用 Yup、Zod,还有 TypeScript 的运行时校验(比如 io-ts)。今天就来唠唠这些方案在真实项目里的表现,哪些让我省心,哪些让我半夜想删库跑路。

手写校验:灵活但累成狗

最原始的办法,就是每个接口入口处写一堆 if-else:

function createUser(data) {
  if (!data.name || typeof data.name !== 'string') {
    throw new Error('name is required and must be string');
  }
  if (typeof data.age !== 'number' || data.age < 0) {
    throw new Error('age must be a non-negative number');
  }
  // ... 继续写下去
}

优点?当然有——完全掌控,想怎么校验就怎么校验,连“手机号必须以 13 开头”这种奇葩需求都能塞进去。但缺点更明显:重复代码爆炸,改一个字段要改 N 个地方,而且容易漏。我之前在一个表单里漏了邮箱格式校验,结果用户注册了一堆 abc@ 这种垃圾数据,清理起来头大。

除非是极简脚本或临时 demo,否则我真不推荐。你的时间不值钱吗?

Yup:老牌选手,但有点“重”

Yup 是我早期用得最多的方案,尤其配合 Formik 做表单验证简直标配。定义 schema 很直观:

import * as yup from 'yup';

const userSchema = yup.object({
  name: yup.string().required(),
  age: yup.number().min(0).required(),
  email: yup.string().email().required()
});

// 使用
try {
  await userSchema.validate(data, { abortEarly: false });
} catch (err) {
  console.log(err.errors); // 所有错误信息
}

Yup 的优势在于生态成熟,社区插件多,异步校验也支持(比如检查用户名是否已存在)。但问题也不少:

  • 体积不小,gzip 后还有 10KB+,对轻量项目不太友好
  • 类型推导弱,TypeScript 里得额外加 @types/yup,而且不能自动从 schema 推出 interface,得手动同步
  • API 有点啰嗦,.string().required() 写多了手酸

我去年一个中后台项目用 Yup,结果打包体积被它拖慢了 0.5s,老板问起来我还得解释半天。现在除非是已有 Formik 项目,否则我基本不碰它了。

Zod:真香!我现在的首选

自从用了 Zod,我感觉自己以前在参数验证上浪费了太多生命。它和 TypeScript 集成得近乎完美——你定义 schema,它自动生成类型,反过来也行:

import { z } from 'zod';

const UserSchema = z.object({
  name: z.string().min(1),
  age: z.number().min(0),
  email: z.string().email()
});

type User = z.infer<typeof UserSchema>; // 自动推导出 TS 类型!

// 校验
const result = UserSchema.safeParse(data);
if (!result.success) {
  console.log(result.error.flatten()); // 结构化错误
  return;
}
// result.data 就是类型安全的 User 对象

我特别喜欢它的几点:

  • 零依赖,打包后只有 5KB 左右
  • 类型推导丝滑,改 schema 就等于改类型,不用两边维护
  • API 简洁,链式调用很舒服,还支持 transform、default 等高级操作
  • 错误信息结构清晰,前端直接拿来展示都行

上周我重构一个 API 层,把所有手写校验换成 Zod,代码量少了 40%,还顺手修复了两个隐藏的类型漏洞。唯一的小坑是:它的 .parse() 会抛异常,得用 .safeParse() 才能静默处理——我一开始不知道,测试时直接崩了,折腾了半天才反应过来。

TypeScript 运行时校验(io-ts):强大但学习成本高

io-ts 是另一种思路:用 TypeScript 的类型系统反向生成运行时校验。听起来很酷,但实际用起来有点绕:

import * as t from 'io-ts';

const User = t.type({
  name: t.string,
  age: t.number,
  email: t.string
});

type User = t.TypeOf<typeof User>;

// 校验
const result = User.decode(data);
if (result._tag === 'Left') {
  console.log(result.left); // 错误详情
}

它的优势是理论完备性高,适合超大型系统或对类型安全有极致要求的场景(比如金融)。但缺点也很致命:

  • API 不直观,.decode() 返回 Either 类型,处理起来啰嗦
  • 文档晦涩,新手容易懵
  • 生态不如 Zod 活跃,社区方案少

我试过在一个内部工具里用 io-ts,结果团队新人看了直摇头,说“这比写正则还难”。除非你团队全是 FP 爱好者,否则真没必要硬上。

我的选型逻辑:看场景,别死磕

总结一下我的实战选择:

  • 新项目(TS + React/Vue):无脑选 Zod。类型安全、轻量、API 友好,三者兼得,何乐不为?
  • 老项目(JS + Formik):继续用 Yup,迁移成本太高,不如留着。
  • 超简单脚本或临时接口:手写几行 if 也行,别为了校验引入一整套库。
  • 金融/医疗等强合规场景:可以考虑 io-ts,但务必评估团队接受度。

另外提醒一点:参数验证只是防线的第一环。再好的 schema 也防不住恶意攻击,记得配合 CSP、CORS、速率限制等安全措施。我见过有人以为用了 Zod 就万事大吉,结果被 SQL 注入搞穿了——schema 只管格式,不管内容安全啊!

最后说两句

参数验证这事,看似简单,实则影响整个系统的健壮性。我踩过坑,也试过各种方案,现在基本固定用 Zod 了。它不是银弹,但在我经手的 80% 项目里都是最优解。

以上是我个人对参数验证方案的对比总结,有更优的实现方式欢迎评论区交流。如果你也在用 Zod,不妨分享下你的踩坑经验?

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论