一. 介绍
在做 FastAPI / AI知识库 / 文件上传系统时,经常会遇到一个问题:
❗ 用户上传的文件可能是伪造的(比如 .exe 改成 .pdf)
因此我们不能只靠"文件后缀名",必须做多重校验:
✔ 文件后缀
✔ MIME 类型
✔ 文件头(magic number)
二、整体设计思路
这个工具分为两部分:
✔ 1. 结果对象(FileCheckResult)
用于统一返回校验结果
✔ 2. 校验工具类(FileTypeChecker)
封装核心校验逻辑
三、完整代码实现
python
from dataclasses import dataclass
from typing import BinaryIO, Optional
import mimetypes
@dataclass
class FileCheckResult:
"""
文件校验结果结构体
"""
is_valid: bool # 是否通过校验
file_type: Optional[str] # 文件类型:pdf / doc / docx
reason: str = "" # 不通过原因
class FileTypeChecker:
"""
文件类型校验器(工业级工具类)
"""
# MIME 类型白名单
ALLOWED_MIME = {
"pdf": ["application/pdf"],
"doc": ["application/msword"],
"docx": [
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
]
}
@staticmethod
def check(filename: str, file: BinaryIO) -> FileCheckResult:
"""
校验文件是否为 PDF / Word 文件
"""
filename_lower = filename.lower()
# =========================
# 1️⃣ MIME 类型判断
# =========================
mime, _ = mimetypes.guess_type(filename_lower)
# =========================
# 2️⃣ 文件头判断(magic number)
# =========================
header = file.read(8)
file.seek(0) # ⚠️ 重置指针(非常重要)
# =========================
# 3️⃣ PDF 校验
# =========================
if filename_lower.endswith(".pdf"):
if header.startswith(b"%PDF") and mime in FileTypeChecker.ALLOWED_MIME["pdf"]:
return FileCheckResult(True, "pdf")
return FileCheckResult(False, reason="非法PDF文件或文件已损坏")
# =========================
# 4️⃣ DOCX 校验(本质是 ZIP)
# =========================
if filename_lower.endswith(".docx"):
if header[:2] == b"PK" and mime in FileTypeChecker.ALLOWED_MIME["docx"]:
return FileCheckResult(True, "docx")
return FileCheckResult(False, reason="非法DOCX文件或文件已损坏")
# =========================
# 5️⃣ DOC 校验(OLE格式)
# =========================
if filename_lower.endswith(".doc"):
if header.startswith(b"\xD0\xCF\x11\xE0") and mime in FileTypeChecker.ALLOWED_MIME["doc"]:
return FileCheckResult(True, "doc")
return FileCheckResult(False, reason="非法DOC文件或文件已损坏")
# =========================
# 6️⃣ 不支持的文件类型
# =========================
return FileCheckResult(False, reason="仅支持 PDF / DOC / DOCX 文件")
四、核心知识点讲解
1. 为什么不能只判断后缀?
例如:
python
virus.exe → 改名 → report.pdf
如果只判断后缀,会被绕过
2. MIME 类型是什么?
MIME 是系统对文件的"身份描述"。
| 文件 | MIME |
|---|---|
| application/pdf | |
| DOC | application/msword |
| DOCX | openxml格式 |
3. 文件头(magic number)
这是文件真正的"身份证"。
| 文件 | MIME |
|---|---|
| DOC | PK(zip结构) |
| DOCX | D0 CF 11 E0 |
4. 为什么要 file.seek(0)?
file.read(8)
会移动文件指针
如果不重置:
后续读取 → 数据丢失 ❌
所以必须:
file.seek(0)
五、使用方式(FastAPI 示例)
python
from fastapi import UploadFile, File, HTTPException
@router.post("/upload")
async def upload(file: UploadFile = File(...)):
result = FileTypeChecker.check(file.filename, file.file)
if not result.is_valid:
raise HTTPException(status_code=400, detail=result.reason)
return {
"filename": file.filename,
"type": result.file_type
}