Become a sponsor

本章从配置到使用,完整说明文件上传功能的实现方式,包括后端接口、前端组件、安全机制和最佳实践。
前端 Vue3 后端 FastAPI 文件系统
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Upload │ multipart │ 安全校验 │ 写入 │ uploads/ │
│ 组件 │ ─────────> │ ─ 扩展名白名单│ ───────> │ 2026/ │
│ │ │ ─ 魔术字节 │ │ 09/ │
│ │ │ ─ 大小限制 │ │ 05/ │
│ │ │ ─ 路径遍历 │ │ xxx.jpg│
└──────────┘ └──────────────┘ └──────────┘
│
▼
┌──────────────┐
│ 返回 fileUrl │
│ 前端存入字段 │
└──────────────┘| 变量 | 默认值 | 说明 |
|---|---|---|
UPLOAD_ALLOWED_EXTS | .jpg,.jpeg,.png,.gif,...,.zip | 允许上传的文件扩展名(逗号分隔,带点号) |
UPLOAD_MAX_SIZE_MB | 10 | 上传文件大小上限(MB) |
FASTAPI_FILE_URL | http://file.fastapi.elevue | 文件访问域名 |
FASTAPI_UPLOAD_DIR | uploads | 上传文件存储目录 |
系统提供统一的文件上传入口,支持图片、文档、压缩包等各类文件:
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成功响应:
{
"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 前缀,无可靠二进制签名)不仅检查扩展名,还读取文件头部魔术字节验证真实类型:
| 类型 | 魔术字节 |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 |
| GIF | 47 49 46 38 |
25 50 44 46 | |
| ZIP | 50 4B 03 04 |
防止攻击者通过修改扩展名上传恶意文件。
文件名经过安全校验,拒绝包含 .. 或路径分隔符的文件名:
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 限制立即中断,避免整文件读入内存:
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), Noneuploads/
├── 2026/
│ ├── 09/
│ │ ├── 05/
│ │ │ ├── 2026090514302512345.jpg
│ │ │ ├── 2026090515011278901.pdf
│ │ │ └── ...
│ │ └── 06/
│ └── 10/
└── 2025/{UPLOAD_DIR}/{YYYYMMDD}/),避免单目录文件过多年月日时分秒 + 5位随机数 重命名,避免中文文件名和冲突上传成功后返回的 fileUrl 是拼接了 FASTAPI_FILE_URL 前缀的完整 URL:
http://127.0.0.1:8031/api/file/20250306/2025030614302512345.jpgService 层的 BaseService._enrich_file_urls() 会自动为文件字段补此前缀。
<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><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 补全:
class ArticleService(BaseService[Article]):
# 文件字段:add/update 时自动迁移临时文件,列表/详情自动补全 URL
file_fields = ('cover', 'attachment')FASTAPI_FILE_URL 前缀在 Service 中声明 rich_text_fields,基类自动处理:
class ArticleService(BaseService[Article]):
# 富文本字段:自动迁移嵌入图片 + XSS 清洗
rich_text_fields = ('content',)<img src="..."> 标签原因:文件大小超过 UPLOAD_MAX_SIZE_MB 限制。
解决:修改 .env 中的 UPLOAD_MAX_SIZE_MB 值。
原因:文件扩展名不在白名单中。
解决:检查 UPLOAD_ALLOWED_EXTS 配置,添加所需扩展名。
原因:FASTAPI_FILE_URL 配置错误或前端未拼接域名。
解决:检查 .env 中的 FASTAPI_FILE_URL 配置,确保前端正确拼接。
原因:演示模式下写操作被拦截。
解决:将 .env 中的 FASTAPI_DEMO 设为 False。
文件上传功能通过扩展名白名单 + 魔术字节校验 + 路径遍历防护 + 大小限制四层安全机制保障安全性。文件按日期分目录存储,使用时间戳 + 随机数重命名。统一的 uploadFile 接口支持图片和各类文件上传。Service 层通过 file_fields 和 rich_text_fields 声明即可自动处理文件迁移和 URL 补全。