Skip to content

使用 Zod 验证确认密码

问题概述

在表单验证中,确认密码字段是常见的需求。用户需要输入两次相同的密码以确保没有输入错误。本文将介绍如何使用 Zod 验证库实现密码和确认密码的匹配验证。

常见实现难点:

  • 如何在 Zod 对象中实现跨字段验证
  • 如何将错误精准显示在确认密码字段
  • 如何避免冗余验证逻辑
  • 确保错误信息的友好提示

推荐解决方案:使用 .refine()

Zod 的 refine() 方法是实现密码匹配验证的最佳方式:

ts
export const registerUserSchema = z.object({
  firstName: z.string(),
  lastName: z.string(),
  userName: z.string(),
  email: z.string().email(),
  phone: z.string().transform(data => Number(data)),
  password: z.string().min(4),
  confirmPassword: z.string().min(4),
  avatar: z.string().optional(),
  isVerified: z.boolean().optional()
}).refine(
  (data) => data.password === data.confirmPassword,
  {
    message: "密码不匹配!",
    path: ["confirmPassword"],
  }
);

实现原理:

  1. 条件检查:使用箭头函数比较 passwordconfirmPassword
  2. 错误处理
    • message 字段定义错误提示文本
    • path 将错误关联到特定字段 (confirmPassword)
  3. 验证顺序
    • 先验证基础规则(如最小长度)
    • 基础验证通过后执行密码匹配检查

重要提示

密码字段和确认密码字段都必须设置基础验证规则(如 .min(4))。否则如果用户不填写密码,错误会出现在 confirmPassword 字段。

替代方法:使用 .superRefine()

当需要更复杂的验证逻辑时,可以使用 superRefine()

ts
export const registerUserSchema = z.object({
  // ...其他字段定义
}).superRefine(({ confirmPassword, password }, ctx) => {
  if (confirmPassword !== password) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "密码不匹配",
      path: ['confirmPassword']
    });
  }
});

两种方法对比

特性.refine().superRefine()
代码复杂度简单直观相对复杂
适用场景简单条件检查需要多条错误信息或复杂验证
错误反馈单字段错误支持多次 addIssue 调用
推荐指数⭐⭐⭐⭐⭐⭐

最佳实践

  1. 基础验证(如密码长度)放在字段定义中
  2. 跨字段匹配验证放在最后使用 refine
  3. path 参数中明确指定错误关联字段
  4. 错误信息应清晰指明问题所在位置

完整示例与使用场景

ts
// 包含密码强度的完整注册表单验证
export const registerUserSchema = z.object({
  // ...其他字段
  password: z.string()
    .min(8, "密码至少需要8个字符")
    .regex(/[A-Z]/, "必须包含大写字母")
    .regex(/[0-9]/, "必须包含数字"),
  confirmPassword: z.string()
})
.refine(data => !!data.confirmPassword, {
  message: "请确认密码",
  path: ["confirmPassword"]
})
.refine(data => data.password === data.confirmPassword, {
  message: "两次输入的密码不匹配",
  path: ["confirmPassword"]
});

使用场景解析

React Hook Form 集成

tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const FormComponent = () => {
  const { register, formState: { errors } } = useForm({
    resolver: zodResolver(registerUserSchema)
  });

  return (
    <form>
      {/* ...其他字段 */}
      <input 
        {...register("password")} 
        placeholder="密码" />
      {errors.password && <span>{errors.password.message}</span>}
      
      <input
        {...register("confirmPassword")}
        placeholder="确认密码" />
      {/* 将显示密码不匹配错误 */}
      {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
    </form>
  );
}

Node.js 后端验证

ts
app.post('/register', (req, res) => {
  try {
    const data = registerUserSchema.parse(req.body);
    // 处理注册逻辑
  } catch (err) {
    if (err instanceof z.ZodError) {
      // 返回验证错误详情
      return res.status(400).json(err.errors);
    }
  }
});

架构建议

对于复杂的表单,建议拆分验证逻辑:

ts
// 密码相关规则
const passwordSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string()
}).refine(/* 匹配规则 */);

// 主表单组合
const mainSchema = baseSchema.merge(passwordSchema);

常见问题解决

1. 错误未出现在正确位置

确保在 refine 的配置中正确设置了 path 参数:

ts
.refined(..., {
  // 必须指定路径
  path: ["confirmPassword"] 
})

2. TypeScript 类型不匹配

使用 .transform() 后可能出现的类型问题:

ts
phone: z.string().transform(data => Number(data))
.useEffect(v => v.trim()) // 错误:数字没有trim方法

解决方案:

ts
phone: z.string().min(1).trim()
  .refine(val => !isNaN(Number(val)), {
    message: "必须是有效数字"
  })
  .transform(Number)

3. 验证敏感数据

不要在错误信息中直接泄露密码信息:

ts
// 不推荐的写法
.refine(data => data.password === data.confirmPassword, {
  // ❌ 避免在错误中显示具体值
  message: `输入的值${data.confirm}不匹配`
})

// 正确写法
.refine(..., {
  message: "密码不匹配" // ✅ 通用提示
})

总结

  1. 核心方法:使用 refine() 进行密码匹配验证
  2. 最佳实践
    • passwordconfirmPassword 设置相同基础规则
    • 通过 path 参数精准定位错误到确认密码字段
    • 使用清晰的中文错误提示提高用户体验
  3. 复杂场景:使用 superRefine() 处理多条件验证
  4. 安全提示:避免在错误信息中泄露敏感数据

通过 Zod 的验证链设计,开发者可以实现结构清晰、可维护性强的表单验证逻辑,特别是在处理密码确认这类跨字段验证时表现出色。

生产环境注意事项

前端验证不可替代后端验证!所有敏感性操作(如注册、支付)必须同时实现服务端验证,防止恶意绕过前端验证机制。