如何使用 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
});
项目设置
本文需要两个依赖项: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
在终端中运行上述命令后,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;
现在,请将函数留空。稍后您将在继续操作时填写它们。
验证文件输入
HTML 文件元素允许接收任何文件类型,包括视频、音频、图像和文档。定义文件输入时,您可以使用accept
属性指定它应接收的文件类型——例如仅接收图像文件,或者接收视频和音频文件的组合。该accept
属性接受任何有效且唯一文件类型标识符的字符串值,并阻止文件输入接受任何类型与指定类型标识符不对应的文件。使用该accept
属性,您可以限制文件类型,以排除危险格式,例如可执行文件 (.exe)、脚本 (.js) 和启用宏的文档 (.docm)。
文件类型验证
使用 Zod,您还可以定义输入应接受的文件类型,以增加额外的安全保障。您的App.tsx
文件中包含两个文件输入——一个用于图像,另一个用于文档,您需要编写一个 Zod 模式,使用instanceof
和refine
方法分别验证每个文件类型。
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" }
);
该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",
});
与使用方法检查文件类型一样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);
}
};
该validateFile
函数接受 3 个参数——文件、模式和字段。模式 safeParse 包含文件,其结果存储在result
变量中。如果解析不成功,函数会使用error
Zod 返回的错误消息更新状态中失败输入的字段,并且函数返回 false。如果解析成功,则在错误状态下将字段设置为未定义,并且函数返回 true。通过使用 ZodsafeParse
方法,您可以让应用程序优雅地失败,从而防止 Zod 抛出错误,避免造成糟糕的用户体验。handleDocChange
和handleImgChange
分别是传递给 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",
}
),
});
此架构中有两个新的 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([]);
}
};
预防安全风险和处理极端情况
文件验证的主要目的之一是保护您的应用程序免受潜在的安全风险,例如上传隐藏在文件中的恶意脚本。即使对可接受的文件类型有所限制,采取额外措施来防范这些风险也至关重要:
- 验证文件类型和 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