- Published on
如何使用 Zod 库实现文件输入验证
- Authors
- Name
- Zhanxin
Tags
本文共有 1730 字 · 预计阅读时长≈ 9 min
在 Web 开发中,表单验证对于应用的安全性和完整性至关重要。表单作为用户与应用的交互窗口,接收用户输入,也成为恶意用户潜在攻击的入口。攻击者可以通过表单输入恶意脚本或文件,从而威胁应用的完整性并危及用户隐私数据。
尽管文本输入通常是脚本注入的首选目标,但文件输入也存在安全风险。恶意用户可能通过文件上传隐蔽地嵌入恶意脚本。为防范此类风险,有效的表单验证至关重要。
Zod 是一个适用于 JavaScript 和 TypeScript 的数据验证库,提供强类型支持,简化数据验证并增强输入安全性。本文将深入讲解如何使用 Zod 对文件输入进行验证,以保障应用安全。
Zod 简介
Zod 是一个 TypeScript 优先的架构验证库,能够模拟 TypeScript 的强类型安全性,确保运行时的数据结构符合预期。通过 import { z } from 'zod'
引入后,即可使用 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
});
Zod 的语法简洁明了,与 TypeScript 完美结合,是实现数据验证的理想选择。
项目初始化
在本示例中,您需要安装 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
设置完成后,启动本地服务器
npm run dev
现在,应用程序已成功运行,删除 App.css 文件,清除 App.tsx 和 index.css 文件,并将前者替换为下面的代码块。
// 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 属性,可限制允许上传的文件类型,防止用户上传潜在危险文件(如 .exe 文件)。在前端,使用 Zod 可以进一步强化验证,确保文件的类型和大小符合要求。
在 utils/schema.ts
文件中,我们可以定义文件的验证模式。以下示例展示了如何使用 Zod 验证文档和图像文件类型:
// utils/schema
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" }
);
文件大小验证
除了文件类型验证之外,您还可以扩充 schema 验证文件大小。
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",
});
前端验证集成
在 App.tsx 文件中,我们可以引入 Zod Schema 并编写文件验证函数:
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 函数使用 Zod 的 safeParse 方法验证文件,返回结果并在错误时显示自定义消息。
处理多个文件
通过将 multiple 属性添加到您的 input 元素,您可以一次选择并上传多个文件。使用 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",
}
),
});
将其与应用程序的表单输入元素集成
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 类型与扩展名一致,以防止恶意文件伪装。
- 清理文件内容:对于可能含脚本的文件(如 SVG),可以在服务器端进行清理,删除潜在的恶意代码。
- 拒绝可疑文件:不明确或不匹配 MIME 类型和扩展名的文件,应被拒绝上传。
- 此外,文件上传可能遇到空文件、编码问题等边缘情况。合理处理这些问题,能有效提高应用的用户体验与安全性。
总结
Zod 使得文件输入验证更简单、直观,并为应用提供了额外的安全层。结合 TypeScript 使用时,Zod 带来了更强的类型安全保障。掌握表单验证和文件上传的最佳实践,不仅能提高应用安全性,还能让用户体验更加友好。