Files
djangoadmin-wiki/DjangoAdmin_Flask_EleVue/zh/volume4/tools/upload-guide.md
T
2026-09-12 23:12:15 +08:00

296 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 文件上传完整流程
本章从配置到使用,完整说明文件上传功能的实现方式,包括后端接口、前端组件、安全机制和最佳实践。
## 上传架构
```
前端 Vue3 后端 Flask 文件系统
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Upload │ multipart │ 安全校验 │ 写入 │ uploads/ │
│ 组件 │ ─────────> │ ─ 扩展名白名单│ ───────> │ 2026/ │
│ │ │ ─ 魔术字节 │ │ 09/ │
│ │ │ ─ 大小限制 │ │ 05/ │
│ │ │ ─ 路径遍历 │ │ xxx.jpg│
└──────────┘ └──────────────┘ └──────────┘
│
▼
┌──────────────┐
│ 返回 fileUrl │
│ 前端存入字段 │
└──────────────┘
```
## 环境变量配置
| 变量 | 默认值 | 说明 |
|:----|:----|:----|
| `UPLOAD_ALLOWED_EXTS` | `.jpg,.jpeg,.png,.gif,...,.zip` | 允许上传的文件扩展名(逗号分隔,带点号) |
| `UPLOAD_MAX_SIZE_MB` | `10` | 上传文件大小上限(MB) |
| `FLASK_FILE_URL` | `http://file.flask.elevue` | 文件访问域名 |
| `FLASK_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
```
成功响应:
```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:8021/api/file/20250306/2025030614302512345.jpg"
},
"ok": true
}
```
:::tip 富文本编辑器集成
富文本编辑器(如 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` |
| PDF | `25 50 44 46` |
| ZIP | `50 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
def _read_upload_limited(file, max_size_bytes):
chunks = []
total = 0
while True:
chunk = 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` 是拼接了 `FLASK_FILE_URL` 前缀的完整 URL:
```
http://127.0.0.1:8021/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):
# 文件字段:add/update 时自动迁移临时文件,列表/详情自动补全 URL
file_fields = ('cover', 'attachment')
```
### 自动处理逻辑
1. **新增/更新时**:检测文件字段值是否为临时路径,如果是则迁移到正式目录
2. **列表/详情时**:自动为文件字段补全 `FLASK_FILE_URL` 前缀
## 富文本中的图片
在 Service 中声明 `rich_text_fields`,基类自动处理:
```python
class ArticleService(BaseService):
# 富文本字段:自动迁移嵌入图片 + 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` 配置,添加所需扩展名。
### 图片上传成功但不显示
**原因**:`FLASK_FILE_URL` 配置错误或前端未拼接域名。
**解决**:检查 `.env` 中的 `FLASK_FILE_URL` 配置,确保前端正确拼接。
### 上传报演示环境无权限
**原因**:演示模式下写操作被拦截。
**解决**:将 `.env` 中的 `FLASK_DEMO` 设为 `False`。
## 总结
文件上传功能通过扩展名白名单 + 魔术字节校验 + 路径遍历防护 + 大小限制四层安全机制保障安全性。文件按日期分目录存储,使用时间戳 + 随机数重命名。统一的 `uploadFile` 接口支持图片和各类文件上传。Service 层通过 `file_fields` 和 `rich_text_fields` 声明即可自动处理文件迁移和 URL 补全。