FastAPI 文件上传校验(PDF / Word)工业级实现方案

一. 介绍

在做 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
PDF application/pdf
DOC application/msword
DOCX openxml格式
3. 文件头(magic number)

这是文件真正的"身份证"。

文件 MIME
PDF %PDF
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
    }