Skip to content

文件上传完整流程

本章从配置到使用,完整说明文件上传功能的实现方式,包括后端接口、前端组件、安全机制和最佳实践。

上传架构

前端 Vue3                后端 FastAPI              文件系统
┌──────────┐            ┌──────────────┐          ┌──────────┐
│ Upload   │  multipart │  安全校验     │  写入    │ uploads/ │
│ 组件     │ ─────────> │  ─ 扩展名白名单│ ───────> │ 2026/    │
│          │            │  ─ 魔术字节   │          │   09/    │
│          │            │  ─ 大小限制   │          │   05/    │
│          │            │  ─ 路径遍历   │          │   xxx.jpg│
└──────────┘            └──────────────┘          └──────────┘


                        ┌──────────────┐
                        │ 返回 fileUrl │
                        │ 前端存入字段  │
                        └──────────────┘

环境变量配置

变量默认值说明
UPLOAD_ALLOWED_EXTS.jpg,.jpeg,.png,.gif,...,.zip允许上传的文件扩展名(逗号分隔,带点号)
UPLOAD_MAX_SIZE_MB10上传文件大小上限(MB)
FASTAPI_FILE_URLhttp://file.fastapi.elevue文件访问域名
FASTAPI_UPLOAD_DIRuploads上传文件存储目录

上传接口

系统提供统一的文件上传入口,支持图片、文档、压缩包等各类文件:

上传文件

POST /api/v1/upload/uploadFile
Content-Type: multipart/form-data
Authorization: Bearer <token>

file: <文件>

支持格式由 UPLOAD_ALLOWED_EXTS 配置控制,默认包含:

图片:.jpg, .jpeg, .png, .gif, .bmp, .ico
文档:.pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .txt
音视频:.mp3, .mp4
压缩:.zip, .rar, .7z

成功响应:

json
{
    "code": 0,
    "msg": "上传成功",
    "data": {
        "originalName": "photo.jpg",
        "fileExtension": "jpg",
        "fileType": "image/jpeg",
        "fileSize": 306995,
        "fileName": "2025030614302512345.jpg",
        "filePath": "/20250306/2025030614302512345.jpg",
        "fileUrl": "http://127.0.0.1:8031/api/file/20250306/2025030614302512345.jpg"
    },
    "ok": true
}

富文本编辑器集成

富文本编辑器(如 TinyMCE、CKEditor、WangEditor 等)的图片上传同样使用此接口。编辑器配置上传地址为 /api/v1/upload/uploadFile,上传成功后从 response.data.fileUrl 获取图片 URL 即可。

安全机制

扩展名白名单

只允许 UPLOAD_ALLOWED_EXTS 中配置的扩展名。配置的扩展名会与内置安全集合取交集,确保不会绕过安全校验。留空时回落到内置智能集合。

禁止上传的扩展名(硬编码黑名单,无论配置如何一律拒绝):

.exe, .bat, .cmd, .sh, .ps1, .php, .asp, .aspx, .jsp, .py, .rb, .js, .html, .htm
.svg(无魔数签名,且存在存储型 XSS 风险)
.webp(魔数仅 RIFF 前缀,无可靠二进制签名)

魔术字节校验

不仅检查扩展名,还读取文件头部魔术字节验证真实类型:

类型魔术字节
JPEGFF D8 FF
PNG89 50 4E 47
GIF47 49 46 38
PDF25 50 44 46
ZIP50 4B 03 04

防止攻击者通过修改扩展名上传恶意文件。

路径遍历防护

文件名经过安全校验,拒绝包含 .. 或路径分隔符的文件名:

python
def _is_safe_filename(filename: str) -> bool:
    normalized = os.path.normpath(filename)
    if '..' in normalized.split(os.sep):
        return False
    basename = os.path.basename(normalized)
    return basename == normalized

大小限制

上传文件通过流式读取校验大小,超过 UPLOAD_MAX_SIZE_MB 限制立即中断,避免整文件读入内存:

python
async def _read_upload_limited(file, max_size_bytes):
    chunks = []
    total = 0
    while True:
        chunk = await file.read(65536)
        if not chunk:
            break
        total += len(chunk)
        if total > max_size_bytes:
            return None, R.failed(msg="文件大小超过限制")
        chunks.append(chunk)
    return b''.join(chunks), None

文件存储策略

目录结构

uploads/
├── 2026/
│   ├── 09/
│   │   ├── 05/
│   │   │   ├── 2026090514302512345.jpg
│   │   │   ├── 2026090515011278901.pdf
│   │   │   └── ...
│   │   └── 06/
│   └── 10/
└── 2025/
  • 按日期分目录存储({UPLOAD_DIR}/{YYYYMMDD}/),避免单目录文件过多
  • 文件名使用 年月日时分秒 + 5位随机数 重命名,避免中文文件名和冲突
  • 保留原始扩展名

文件访问 URL

上传成功后返回的 fileUrl 是拼接了 FASTAPI_FILE_URL 前缀的完整 URL:

http://127.0.0.1:8031/api/file/20250306/2025030614302512345.jpg

Service 层的 BaseService._enrich_file_urls() 会自动为文件字段补此前缀。

前端集成

文件上传组件

vue
<template>
  <el-upload
    :action="uploadUrl"
    :headers="headers"
    :on-success="handleSuccess"
    :before-upload="beforeUpload"
  >
    <el-button type="primary">上传文件</el-button>
  </el-upload>
</template>

<script setup>
import { ref } from 'vue';
import { useUserStore } from '@/store/modules/user';

const uploadUrl = '/api/v1/upload/uploadFile';
const userStore = useUserStore();
const headers = {
    Authorization: `Bearer ${userStore.token}`,
};

function beforeUpload(file) {
    const isLt10M = file.size / 1024 / 1024 < 10;
    if (!isLt10M) {
        ElMessage.error('文件大小不能超过 10MB');
    }
    return isLt10M;
}

function handleSuccess(response) {
    if (response.code === 0) {
        // 保存 fileUrl 到业务字段
        form.value.cover = response.data.fileUrl;
    }
}
</script>

图片预览

vue
<template>
  <el-image
    v-if="form.cover"
    :src="getFileUrl(form.cover)"
    :preview-src-list="[getFileUrl(form.cover)]"
    fit="cover"
    style="width: 100px; height: 100px"
  />
</template>

<script setup>
function getFileUrl(path) {
    if (!path) return '';
    if (path.startsWith('http')) return path;
    return import.meta.env.VITE_FILE_URL + path;
}
</script>

业务模块中的文件字段

在 Service 中声明 file_fields,基类自动处理文件迁移和 URL 补全:

python
class ArticleService(BaseService[Article]):
    # 文件字段:add/update 时自动迁移临时文件,列表/详情自动补全 URL
    file_fields = ('cover', 'attachment')

自动处理逻辑

  1. 新增/更新时:检测文件字段值是否为临时路径,如果是则迁移到正式目录
  2. 列表/详情时:自动为文件字段补全 FASTAPI_FILE_URL 前缀

富文本中的图片

在 Service 中声明 rich_text_fields,基类自动处理:

python
class ArticleService(BaseService[Article]):
    # 富文本字段:自动迁移嵌入图片 + XSS 清洗
    rich_text_fields = ('content',)

自动处理逻辑

  1. 新增/更新时:解析富文本中的 <img src="..."> 标签
  2. 将临时路径的图片迁移到正式目录
  3. 替换富文本中的图片路径
  4. 进行 XSS 清洗(使用 bleach 库)

常见问题

上传报 413 Request Entity Too Large

原因:文件大小超过 UPLOAD_MAX_SIZE_MB 限制。

解决:修改 .env 中的 UPLOAD_MAX_SIZE_MB 值。

上传报 403 Forbidden

原因:文件扩展名不在白名单中。

解决:检查 UPLOAD_ALLOWED_EXTS 配置,添加所需扩展名。

图片上传成功但不显示

原因FASTAPI_FILE_URL 配置错误或前端未拼接域名。

解决:检查 .env 中的 FASTAPI_FILE_URL 配置,确保前端正确拼接。

上传报演示环境无权限

原因:演示模式下写操作被拦截。

解决:将 .env 中的 FASTAPI_DEMO 设为 False

总结

文件上传功能通过扩展名白名单 + 魔术字节校验 + 路径遍历防护 + 大小限制四层安全机制保障安全性。文件按日期分目录存储,使用时间戳 + 随机数重命名。统一的 uploadFile 接口支持图片和各类文件上传。Service 层通过 file_fieldsrich_text_fields 声明即可自动处理文件迁移和 URL 补全。

小蚂蚁云团队 · 提供技术支持