如何使用 Zod 验证 TypeScript 中的文件输入

2025-06-04

如何使用 Zod 验证 TypeScript 中的文件输入

表单验证是 Web 开发的一个重要方面,它在维护应用程序的安全性和完整性方面发挥着至关重要的作用。表单是应用程序暴露的部分之一,因为它们接受用户的外部输入。通过这些外部表单输入,恶意用户或攻击者可以输入恶意脚本或文件,从而破坏应用程序的完整性并访问用户的私人数据。
虽然文本输入是攻击者试图将恶意脚本注入应用程序的主要目标,但文件输入(不易直接受到脚本注入攻击)是最隐蔽的入口点,并且不太常见。攻击者可以利用文件上传以不明显的方式嵌入恶意脚本。
表单验证是阻止这些恶意脚本进入应用程序的一种方法。Zod 是一个用于 JavaScript 和 TypeScript 的模式验证库,可帮助强制执行数据结构并验证输入。与典型的验证库不同,Zod 与 TypeScript 无缝集成,在运行时提供强大的类型安全性。在本文中,您将学习如何使用 Zod(一个用于 TypeScript/JavaScript 的模式验证库)验证文件输入。

佐德的概述

Zod 是一个 TypeScript 优先的模式声明和验证库,它模仿了 TypeScript 的强静态类型。它允许您在运行时强制执行类型安全,对于希望获得强类型和可靠验证且不影响任何功能的 TypeScript 开发者来说,Zod 是一个绝佳的选择。Zod
提供了一种强大而灵活的方法来定义数据结构的模式。其模式语法简单易懂,您只需使用从“zod”导入的“z”对象即可。“z”对象包含适用于各种数据类型的方法,例如:


import { z } from "zod";

// Basic primitive schemas
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
const undefinedSchema = z.undefined();
const nullSchema = z.null();

// Object Schema
const userSchema = z.object({
  name: z.string(),
  age: z.number().int(), //built-in method: Only allows integers
  email: z.string().email(), //built-in method: Validates email format
});

// Array schema
const stringArraySchema = z.array(z.string());

// Optional and Nullable types
const userSchema = z.object({
  name: z.string(),
  age: z.number().int().optional(), // age is optional
  email: z.string().email().nullable(), // email can be null
});
Enter fullscreen mode Exit fullscreen mode

项目设置

本文需要两个依赖项:React 和 Zod。使用 Vite 创建 React 应用程序的 Typescript 版本并安装 Zod。

// create react app with vite
npm create vite@latest file-input-validation -- --template react-ts
cd file-input-validation
npm install

// install zod
npm install zod
Enter fullscreen mode Exit fullscreen mode

在终端中运行上述命令后,file-input-validation在代码编辑器中打开目录,然后运行npm run dev以在本地主机中启动应用程序。
现在您的应用程序已成功运行,请删除 .batApp.css文件,清除App.tsx和 .batindex.css文件,并将 .bat 文件替换为下面的代码块。
至于样式,请参见此处


// App.tsx

/* eslint-disable @typescript-eslint/no-explicit-any */
import { DOCUMENT_SCHEMA, IMAGE_SCHEMA } from "./utils/schema";
import { useState, useEffect } from "react";
interface ErrorType {
  img_upload?: string;
  doc_upload?: string;
}
function App() {
  const [docFile, setDocFile] = useState<File | undefined>();
  const [imgFile, setImgFile] = useState<File | undefined>();
  const [imgUrl, setImgUrl] = useState("");
  const [error, setError] = useState<ErrorType>({});
  useEffect(() => {
    if (imgFile) {
      const url = URL.createObjectURL(imgFile);
      setImgUrl(url);
      return () => URL.revokeObjectURL(url);
    }
  }, [imgFile]);

  const handleDocChange = (e: React.ChangeEvent<HTMLInputElement>) => {

  };
  const handleImgChange = (e: React.ChangeEvent<HTMLInputElement>) => {

  };
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

  };
  return (
    <div className="app-container">
      <h1>File Input Validation with Zod</h1>
      <div className="form-container">
        <form className="form" onSubmit={handleSubmit}>
          <div className="formfield">
            <label htmlFor="doc-input">
              <p>Document Input</p>
              <div className="doc-label">
                {docFile?.name ? (
                  <p>{docFile?.name}</p>
                ) : (
                  <p>
                    <span>Browse</span> to upload document here{" "}
                  </p>
                )}
                <p className="size">(5MB Max)</p>
              </div>
            </label>
            <input
              id="doc-input"
              name="doc_upload"
              type="file"
              onChange={handleDocChange}
              accept="application/*"
            />
            {error.doc_upload && <p className="error">{error.doc_upload}</p>}
          </div>
          <div className="formfield">
            <label htmlFor="img-input">
              <p>Image Input</p>
              <div className="image-label">
                {imgUrl ? (
                  <img src={imgUrl} alt="img-input" />
                ) : (
                  <div>
                    <p>
                      <span>Browse</span> to upload image here{" "}
                    </p>
                    <p className="size">(5MB Max)</p>
                  </div>
                )}
              </div>
            </label>
            <input
              id="img-input"
              name="img_upload"
              type="file"
              accept="image/*"
              onChange={handleImgChange}
            />
            {error.img_upload && <p className="error">{error.img_upload}</p>}
          </div>
          <button
            type="submit"
            disabled={
              !!error.doc_upload || !!error.img_upload || !docFile || !imgFile
            }
          >
            Submit
          </button>
        </form>
      </div>
    </div>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

现在,请将函数留空。稍后您将在继续操作时填写它们。

验证文件输入

HTML 文件元素允许接收任何文件类型,包括视频、音频、图像和文档。定义文件输入时,您可以使用accept属性指定它应接收的文件类型——例如仅接收图像文件,或者接收视频和音频文件的组合。该accept属性接受任何有效且唯一文件类型标识符的字符串值,并阻止文件输入接受任何类型与指定类型标识符不对应的文件。使用该accept属性,您可以限制文件类型,以排除危险格式,例如可执行文件 (.exe)、脚本 (.js) 和启用宏的文档 (.docm)。

文件类型验证

使用 Zod,您还可以定义输入应接受的文件类型,以增加额外的安全保障。您的App.tsx文件中包含两个文件输入——一个用于图像,另一个用于文档,您需要编写一个 Zod 模式,使用instanceofrefine方法分别验证每个文件类型。


import { z } from "zod";

// Document Schema
export const DOCUMENT_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "application/pdf",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      ].includes(file.type),
    { message: "Invalid document file type" }
  );

// Image Schema
export const IMAGE_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "image/png",
        "image/jpeg",
        "image/jpg",
        "image/svg+xml",
        "image/gif",
      ].includes(file.type),
    { message: "Invalid image file type" }
  );
Enter fullscreen mode Exit fullscreen mode

instanceof方法检查某个值是否是特定类的实例。在本例中,两种模式都会检查该值是否属于 TypeScript File 类。该refine方法允许您定义自定义验证逻辑。它接受两个参数——一个回调函数,预期在验证失败时返回 false 值;以及一个对象,用于传递可自定义的错误处理行为选项。Zod
接收 File 值,并使用数组中列出的类型检查其类型。如果文件类型在数组中,则文件通过验证。否则,Zod 会发送错误消息。

文件大小验证

除了文件类型验证之外,您还可以扩充架构以再次使用优化方法来验证文件大小。


import { z } from "zod";
const fileSizeLimit = 5 * 1024 * 1024; // 5MB

// Document Schema
export const DOCUMENT_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "application/pdf",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      ].includes(file.type),
    { message: "Invalid document file type" }
  )
  .refine((file) => file.size <= fileSizeLimit, {
    message: "File size should not exceed 5MB",
  });

// Image Schema
export const IMAGE_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "image/png",
        "image/jpeg",
        "image/jpg",
        "image/svg+xml",
        "image/gif",
      ].includes(file.type),
    { message: "Invalid image file type" }
  )
  .refine((file) => file.size <= fileSizeLimit, {
    message: "File size should not exceed 5MB",
  });
Enter fullscreen mode Exit fullscreen mode

与使用方法检查文件类型一样refine,使用相同的方法来检查文件大小,方法是将其与fileSizeLimit上面代码块中定义的值进行比较。如果文件大小超出限制,Zod 将返回定义的错误消息。
这样,您就将两个验证检查耦合在一个模式中,以验证文件类型和大小。

使用 Zod Schemas 进行前端验证

除非与提供待验证值的表单集成,否则这些模式将毫无用处。文件中App.tsx有一些未定义的函数,您将在本节中定义它们。
首先,将您的模式导入到App.tsx文件中,然后定义验证函数。


import { DOCUMENT_SCHEMA, IMAGE_SCHEMA } from "./utils/schema";

const validateFile = (file: File, schema: any, field: keyof ErrorType) => {
    const result = schema.safeParse(file);
    if (!result.success) {
      setError((prevError) => ({
        ...prevError,
        [field]: result.error.errors[0].message,
      }));
      return false;
    } else {
      setError((prevError) => ({
        ...prevError,
        [field]: undefined,
      }));
      return true;
    }
  };
  const handleDocChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const isValid = validateFile(file, DOCUMENT_SCHEMA, "doc_upload");
      if (isValid) setDocFile(file);
    }
  };
  const handleImgChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const isValid = validateFile(file, IMAGE_SCHEMA, "img_upload");
      if (isValid) setImgFile(file);
    }
  };
Enter fullscreen mode Exit fullscreen mode

validateFile函数接受 3 个参数——文件、模式和字段。模式 safeParse 包含文件,其结果存储在result变量中。如果解析不成功,函数会使用errorZod 返回的错误消息更新状态中失败输入的字段,并且函数返回 false。如果解析成功,则在错误状态下将字段设置为未定义,并且函数返回 true。通过使用 ZodsafeParse方法,您可以让应用程序优雅地失败,从而防止 Zod 抛出错误,避免造成糟糕的用户体验。
handleDocChangehandleImgChange分别是传递给 doc_upload 和 img_upload 输入的函数。这两个函数监视上传到各自输入文件的文件,并对validateFile它们运行该函数,并将响应存储在isValid变量中。如果变量为 true,这两个函数会使用文件更新各自的状态。最后,错误状态有助于存储有关每个输入字段的错误信息。每当任何错误状态对象属性包含与其对应的错误时,
此信息就会显示在每个输入字段下方的标签中。<p>

处理多个文件

通过在输入元素中添加该multiple属性,您可以一次选择并上传多个文件。使用 Zod,您可以为 FileList 编写验证模式,就像为 File 编写验证模式一样简单。


const fileSizeLimit = 5 * 1024 * 1024; // 5MB

export const fileUploadSchema = z.object({
  files: z
    .instanceof(FileList)
    .refine((list) => list.length > 0, "No files selected")
    .refine((list) => list.length <= 5, "Maximum 5 files allowed")
    .transform((list) => Array.from(list))
    .refine(
      (files) => {
        const allowedTypes: { [key: string]: boolean } = {
          "image/jpeg": true,
          "image/png": true,
          "application/pdf": true,
          "application/msword": true,
          "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
            true,
        };
        return files.every((file) => allowedTypes[file.type]);
      },
      { message: "Invalid file type. Allowed types: JPG, PNG, PDF, DOC, DOCX" }
    )
    .refine(
      (files) => {
        return files.every((file) => file.size <= fileSizeLimit);
      },
      {
        message: "File size should not exceed 5MB",
      }
    ),
});
Enter fullscreen mode Exit fullscreen mode

此架构中有两个新的 Zod 方法object—— 和transform。 该object方法允许您为对象及其每个属性定义验证架构。 该transform方法允许您将值从一种形式转换为另一种形式。在本例中,FileList 被转换为数组以便于操作。
定义此架构后,您可以像上一节中一样将其与应用程序的表单输入元素集成。


const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const selectedFiles = e.target.files;
  if (!selectedFiles) return;
  const result = fileUploadSchema.safeParse({ files: selectedFiles });
  if (result.success) {
    setFiles(result.data.files);
    setError(null);
  } else {
    setError(result.error.errors[0].message);
    setFiles([]);
  }
};
Enter fullscreen mode Exit fullscreen mode

预防安全风险和处理极端情况

文件验证的主要目的之一是保护您的应用程序免受潜在的安全风险,例如上传隐藏在文件中的恶意脚本。即使对可接受的文件类型有所限制,采取额外措施来防范这些风险也至关重要:

  • 验证文件类型和 MIME 类型:确保上传的文件与其 MIME 类型(例如application/pdf)和扩展名(例如 )匹配.pdf。某些文件类型(例如.svg文件)可能包含脚本,因此必须仔细验证或清理才能允许上传。
  • 清理并限制内容大小:当文件类型可用于包含脚本(例如.svg.docx)时,您可以考虑使用服务器端清理库来驱除任何潜在的有害内容。
  • 拒绝可疑文件类型:如果文件类型不明确,或者其 MIME 类型与文件扩展名不符,则拒绝该文件,并向用户显示一条友好的错误消息。这是一种预防措施,可以防止某些类型的攻击媒介,例如伪装的可执行文件。

除了安全性之外,文件上传还存在许多需要提前处理的边缘情况。以下是一些示例:

  • 空文件上传:用户有时甚至不会注意到他们选择了一个空文件(即大小为 0 字节的文件)。Zod 可以通过设置最小文件大小要求来验证这一点。空文件应该被拒绝,因为实际处理或显示其内容时可能存在错误。
  • 异常文件编码:某些文件使用了不寻常的编码;这可能会导致其他应用程序出错。您可以记录或拒绝包含异常编码的文件。
  • 间歇性上传错误:在实际应用中,用户在文件上传过程中可能会遇到网络中断或其他问题。您可以添加重试机制或错误提示,以便用户能够完成上传。

总而言之,请确保您的验证系统健壮且用户友好。此外,请牢记安全风险和常见的边缘情况,以确保用户与应用程序文件处理功能的交互顺畅、无错误且安全。

结论

通过 Zod 添加文件上传验证功能,可以为您的应用程序增添额外的安全保障,并进一步提升用户体验。Zod 的验证架构让您可以轻松实施全面的检查,并向用户提供清晰的反馈。这些反馈能够帮助用户快速了解并解决其值中的问题。
在不断迭代验证逻辑的过程中,您也在积极构建一个更安全的应用程序,从而提升用户的信任度和满意度。Zod 提供了灵活的文件验证处理方式,能够为任何需要文件处理功能的项目提供坚实的基础,并立即制定此类标准。
有关 Zod 的更全面信息,请参阅文档

请参阅本文的完整项目

文章来源:https://dev.to/drprime01/how-to-validate-a-file-input-with-zod-5739
PREV
React 中的 Jest 测试初学者指南
NEXT
技能问题:如何以 10 倍的速度掌握任何东西,而不会显得愚蠢