手记

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

表单验证是Web开发中的一个关键部分。它在维护应用程序的安全性和完整性方面起着至关重要的作用,因此对于确保应用程序的健康至关重要。表单是应用程序中公开的部分之一,因为它们接收来自用户的外部输入,因此容易受到恶意攻击。恶意用户或攻击者可以输入破坏应用程序完整性的恶意脚本或文件,从而威胁用户私密数据的安全。虽然文本输入是攻击者试图向应用程序注入恶意脚本的主要目标之一,但文件输入虽然不易直接受到脚本注入的攻击,却是最隐蔽的入侵点,且较少被注意。攻击者可以利用文件上传将恶意脚本隐秘地嵌入到应用程序中。表单验证是防止这些恶意脚本进入应用程序的一种方法,并且对于保护应用程序的健康和用户数据的安全至关重要。Zod 是一个用于JavaScript和TypeScript的模式验证库,它能够帮助确保数据结构的正确性和输入的有效性。与一般的验证库不同,Zod 可以与TypeScript无缝集成,提供强大的类型安全性保障。在这篇文章中,你将学习如何使用Zod(一个TypeScript/JavaScript的模式验证库)验证文件输入。

佐德的概述

Zod 是一个以 TypeScript 为主的类型声明和验证库,模仿了 TypeScript 的强静态类型特性。它允许你在运行时强制类型安全,对于希望在不牺牲任何一方的情况下获得强类型和可靠验证的 TypeScript 开发者来说,它是一个绝佳的选择。Zod 提供了一种强大且灵活的方式来定义数据结构的类型。其类型声明的语法简单明了,你只需要从 'zod' 导入 'z' 对象即可。'z' 对象包含了各种数据类型的方法,例如:

    import { z } from "zod";

    // 基本的原始模式定义
    const stringSchema = z.string();
    const numberSchema = z.number();
    const booleanSchema = z.boolean();
    const dateSchema = z.date();
    const undefinedSchema = z.undefined();
    const nullSchema = z.null();

    // 对象模式定义
    const userSchema = z.object({
      name: z.string(),
      age: z.number().int(), // 内置方法:只允许整数
      email: z.string().email(), // 内置方法:验证电子邮件格式
    });

    // 数组模式定义
    const stringArraySchema = z.array(z.string());

    // 可选和可为空类型
    const userSchema = z.object({
      name: z.string(),
      age: z.number().int().optional(), // 年龄是可选的,电子邮件可以为空
      email: z.string().email().nullable(), 
    });

全屏显示 退出全屏

项目启动

要完成这篇文章里的任务,你需要两个依赖项:React 和 Zod。然后,使用 Vite 创建一个基于 Typescript 的 React 应用程序,并安装 Zod。

    // 使用 Vite 创建一个 React 项目(最新版本)
    npm create vite@latest file-input-validation -- --template react-ts
    cd file-input-validation
    npm install

    // 接着安装 zod 依赖
    npm install zod

点击进入全屏,点击退出全屏

运行上述命令后,在你的代码编辑器中打开 file-input-validation 目录,并运行 npm run dev 以在本地服务器上启动应用。
成功运行应用后,删除 App.css 文件,清空 App.tsx 文件的内容,然后用下面的代码替换 App.tsx 文件中的内容。
有关样式,你可以在这里查看:这里

    // 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>使用 Zod 验证文件输入</h1>
          <div className="form-container">
            <form className="form" onSubmit={handleSubmit}>
              <div className="formfield">
                <label htmlFor="doc-input">
                  <p>文档输入</p>
                  <div className="doc-label">
                    {docFile?.name ? (
                      <p>{docFile?.name}</p>
                    ) : (
                      <p>
                        <span>选择</span>上传文件到此处{" "}
                      </p>
                    )}
                    <p className="size">(最大 5MB)</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>图片输入</p>
                  <div className="image-label">
                    {imgUrl ? (
                      <img src={imgUrl} alt="img-input" />
                    ) : (
                      <div>
                        <p>
                          <span>选择</span>上传图片文件到此处{" "}
                        </p>
                        <p className="size">(最大 5MB)</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
                }
              >
                点击提交
              </button>
            </form>
          </div>
        </div>
      );
    }
    export default App;

全屏模式 退出全屏

先别给这些函数写内容。当你继续往下写代码,你之后再回头来补全它们。

文件输入:

验证

HTML文件元素可以接受任何类型的文件,包括视频、音频、图像和文档。在定义文件输入时,你可以使用accept属性指定应该接收的文件类型——可能是仅图像文件,或者是视频和音频文件的混合。accept属性接受任何有效的唯一文件类型标识符字符串,并防止接受不符合指定类型的文件。使用accept属性,你可以排除危险格式,例如可执行文件(.exe)、脚本(.js)和带有宏的文档(.docm)。

文件类型检查

使用 Zod,你还可以定义输入可以接受的文件类型,作为额外的安全层。在你的 App.tsx 文件中有两个文件输入——一个用于图片的输入,另一个用于文档的输入,你将分别为这两个输入编写一个 Zod 架构定义,使用 instanceofrefine 方法进行验证。

    import { z } from "zod";

    // 文档模式定义
    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: "文档类型无效" }
      );

    // 图像模式定义
    export const IMAGE_SCHEMA = z
      .instanceof(File)
      .refine(
        (file) =>
          [
            "image/png",
            "image/jpeg",
            "image/jpg",
            "image/svg+xml",
            "image/gif",
          ].includes(file.type),
        { message: "图像类型无效" }
      );

进入全屏 退出全屏

instanceof 方法用来检查一个值是否是特定类的实例。在这种情况下,两个模式都用来检查该值是否为 TypeScript 文件类的实例。refine 方法允许你定义自定义验证逻辑。它接收两个参数——一个回调函数,该函数应在验证失败时返回 false,以及一个用于自定义错误处理选项的对象。
Zod 接收文件值并使用列表中的类型来检查。如果文件类型在列表中,文件就会通过验证,否则就会抛出错误信息。

文件大小检查

除了验证文件类型之外,你还可以通过再次使用“refine 方法”来检查文件大小,从而扩展验证模式。

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

    // 文档模式 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: "这不是有效的文档文件类型" }
      )
      .refine((file) => file.size <= fileSizeLimit, {
        message: "文件大小不能超过5MB",
      });

    // 图像模式 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: "这不是有效的图像文件类型" }
      )
      .refine((file) => file.size <= fileSizeLimit, {
        message: "文件大小不能超过5MB",
      });

进入全屏 退出全屏

就像检查文件类型那样,我们也是通过 refine 方法来检查文件大小。通过与代码块中定义的 fileSizeLimit 进行比较,如果文件大小超出限制,Zod 会返回相应的错误消息。
这样,你就可以在一个模式中同时验证文件类型和大小了。

使用 Zod 进行前端验证:

这些模式如果没有与提供要验证的值的表单集成,就没有用处。在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);
        }
      };

全屏模式 退出全屏

validateFile 函数接受 3 个参数——文件、模式定义和字段。模式定义会使用 safeParse 方法来解析文件,解析结果会被存储在 result 变量中。如果解析失败,函数会更新 error 状态中的错误信息为 Zod 返回的信息,并返回 false。如果解析成功,则将该字段在错误状态中设置为 undefined 并返回 true。这使得应用程序能够优雅地失败,避免 Zod 抛出错误,从而避免给用户带来不好的体验。

handleDocChangehandleImgChange 是您分别传递给 doc_uploadimg_upload 输入的函数。这些函数会监控上传到各自输入的文件,并对这些文件运行 validateFile 函数,将响应存储在 isValid 变量中。如果该变量为 true,则这些函数会更新其状态为文件。

最后,错误状态用来存储每个字段的错误信息。这些信息会在每个输入字段下方的 <p> 标签中显示,只要有任何错误状态对象属性包含一个与之对应的错误。

多个文件的处理

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

    const 文件大小限制 = 5 * 1024 * 1024; // 5MB,即5兆字节

    export const 文件上传规则 = z.object({
      files: z
        .instanceof(FileList)
        .refine((列表) => 列表.length > 0, "未选择文件")
        .refine((列表) => 列表.length <= 5, "最多5个文件")
        .transform((列表) => Array.from(列表))
        .refine(
          (文件) => {
            const 允许的类型: { [key: string]: boolean } = {
              "image/jpeg": true,
              "image/png": true,
              "application/pdf": true,
              "application/msword": true,
              "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
                true,
            };
            return 文件.every((file) => 允许的类型[file.type]);
          },
          { message: "无效的文件类型。允许的类型:JPEG、PNG、PDF、DOC、DOCX" }
        )
        .refine(
          (文件) => {
            return 文件.every((file) => file.size <= 文件大小限制);
          },
          {
            message: "文件大小不得超过5MB,即5兆字节",
          }
        ),
    });

全屏模式 退出全屏

这个模式中有两个新增的 Zod 方法——objecttransformobject 方法允许你为一个对象及其每个属性定义验证模式。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([]);
      }
    };

// 一个处理文件变化的函数,当文件选择完成后,会解析文件并进行校验。如果校验通过,则设置文件,并清除错误信息;如果校验未通过,则设置错误信息,并清空文件列表。

全屏,恢复

如何预防安全风险及应对特殊情况

文件校验的主要目的之一是保护你的应用程序不受潜在安全风险的影响,这些风险可能包括通过文件上传隐藏的恶意脚本。即使限制接受的文件类型,采取额外的保护措施仍然非常必要。

  • 验证文件类型和 MIME 类型:确保上传的文件与它们的 MIME 类型(例如 application/pdf)和扩展名(例如 .pdf)一致。某些文件类型,如 .svg 文件,可能包含脚本,因此必须仔细验证或清理这些文件,以确保它们可以被安全使用。
  • 清理并限制内容大小:对于可以包含脚本的文件类型,例如 .svg.docx 等,可以考虑使用服务器端清理库来移除任何潜在有害的脚本内容。
  • 拒绝可疑文件类型:如果文件类型不明确或 MIME 类型与文件扩展名不一致,则应拒绝该文件,并向用户显示一个友好的错误消息。这是预防措施,可以防止某些类型的攻击,例如伪装成其他文件类型的可执行文件。

除了安全之外,文件上传确实存在许多特殊情况,需要提前妥善解决。以下是一些例子说明:

  • 上传空文件:用户可能没有注意到选择的是一个空文件。Zod 可以通过设定最小文件大小要求来检验这一点。应该拒绝空文件,因为处理或显示其内容时可能出现问题。
  • 不常见的编码格式:一些文件使用不常见的编码格式;这可能引发其他应用程序的错误。可以记录或拒绝那些编码不常见的文件。
  • 间歇性上传错误:在实际环境中,用户可能在上传文件时遇到网络中断或其他问题。可以添加重试机制或错误提示,帮助用户完成上传。

总之,让验证系统既稳健又易于用户操作。同时,注意可能的安全风险和常见边界情况,确保用户与应用程序的文件处理功能交互时一切顺利、准确无误且安全无忧。

结论:

添加 Zod 的文件上传验证可以增加一层安全性,并进一步提升应用程序的用户体验。Zod 验证方案使得全面的检查变得简单,并能为用户提供清晰的反馈。这种反馈让用户能够快速识别并解决输入中的问题。
随着您不断改进验证逻辑,您正在积极构建一个更安全的应用程序,从而增强用户信任和满意度。Zod 提供了灵活的文件验证方案,为任何需要文件处理功能的项目提供了一套现成的标准。
更多关于 Zod 的详细信息,请参阅 文档

点击这里查看本文完整项目 链接

0人推荐
随时随地看视频
慕课网APP