14 · 文件上传与对象存储
用户传头像、传文档、传大文件——后端要接得住、存得对、出得去。本篇讲清上传链路、大文件分片、对象存储选型。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 文件从客户端到存储再到下载的完整链路,以及"别把文件放应用服务器" |
| 核心内容 | 上传链路设计、文件校验、大文件分片、对象存储(S3 兼容)、下载与防盗链 |
| 读完能回答 | 文件应该存哪?大文件怎么传不卡?怎么校验文件是真的、不超限、不带毒? |
| 相关板块 | C09 · 服务端语言上手 | C16 · 服务端安全纵深 | C12 · 错误处理日志配置 |
一、文件该存哪
| 方案 | 适用 | 问题 |
|---|---|---|
| 应用服务器本地盘 | 临时、单机 | 多实例不一致、重启丢、扩容难 |
| 对象存储(S3/OSS/COS) | 生产推荐 | 弹性、便宜、自带 CDN |
| 数据库 BLOB | 极少量小文件 | 撑爆 DB,别用 |
核心原则:文件放对象存储,数据库只存元数据(URL、大小、类型、上传时间)。应用服务器是无状态服务,文件不应粘在上面。
二、上传链路
2.1 两种方式
| 方式 | 流程 | 适用 |
|---|---|---|
| 服务端中转 | 客户端 → 后端 → 对象存储 | 需要校验/限流/权限控制 |
| 直传(预签名 URL) | 客户端拿签名 URL 直传对象存储 | 大文件、省后端带宽 |
大文件推荐预签名直传:后端只签发带权限的 URL,文件不经过应用服务器,省流量、省压力。
python
from fastapi import UploadFile
import boto3
s3 = boto3.client("s3", endpoint_url=os.environ["S3_ENDPOINT"])
@app.post("/upload")
async def upload(file: UploadFile):
data = await file.read()
key = f"uploads/{uuid4().hex}-{file.filename}"
s3.put_object(Bucket=os.environ["BUCKET"], Key=key, Body=data)
return {"url": f"/files/{key}"}2.2 必须做的校验
python
MAX_SIZE = 10 * 1024 * 1024
@app.post("/upload")
async def upload(file: UploadFile):
if file.size > MAX_SIZE:
raise HTTPException(413, "文件过大")
if file.content_type not in {"image/png", "image/jpeg"}:
raise HTTPException(415, "不支持的类型")| 校验项 | 说明 |
|---|---|
| 大小限制 | 超限 413 |
| 类型校验 | 按 content_type 和实际内容魔数双重校验 |
| 文件名 | 丢弃客户端文件名,自己生成(防路径穿越) |
| 病毒/危险内容 | 必要时接扫描服务 |
⚠️ 不要信任
file.filename:客户端可传../../etc/passwd。服务端一律用自己的随机 key。
三、大文件分片上传
超过几十 MB 时,单次上传容易超时/中断。方案:
| 组件 | 说明 |
|---|---|
| 前端分片 | 切成 e.g. 5MB 一片,逐片上传 |
| 后端/存储合并 | S3 multipart、OSS 分片 |
| 断点续传 | 记录已传分片,失败重传 |
| 秒传 | 先算文件哈希,命中即跳过 |
简化套路:客户端上传每片(带
upload_id+ 片号),全部传完调"合并"接口;存进度到 Redis/DB。
四、下载与访问控制
python
@app.get("/files/{key}")
def download(key: str):
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": BUCKET, "Key": key},
ExpiresIn=300,
)
return {"url": url}| 场景 | 方案 |
|---|---|
| 公开文件(头像) | 直接走 CDN 公开 URL |
| 私有文件 | 预签名 URL(带过期),或经后端鉴权转发 |
| 防盗链 | 对象存储/CDN 配 Referer、签名 |
自检清单
- [ ] 能说清"文件放对象存储、DB 只存元数据"的原则
- [ ] 会做大小、类型(含魔数)、文件名校验
- [ ] 知道大文件用分片/直传,而不是单次上传
- [ ] 会用预签名 URL 控制私有文件访问
常见误区
- 误区:文件存数据库最稳。 撑爆 DB、备份变大,几乎永远是错方案。
- 误区:信任客户端文件名。 路径穿越、覆盖攻击都从这里来,自己生成 key。
- 误区:只校验后缀。 改个扩展名就绕过,要按内容魔数校验。
- 误区:大文件单次传。 超时、中断、无法续传,要分片。
动手练习
练习 A:头像上传
用本地 MinIO(S3 兼容)实现头像上传 + 校验 + 预签名下载。
练习 B:文件名校验
故意传一个 ../../evil.png,确认服务端生成的是自己的随机 key。
练习 C:分片思路
设计(不实现)一个大文件分片上传的状态机:列出前端需要传哪些参数、服务端怎么记录进度、失败怎么续传。
相关文档
本区
- 09 · 服务端语言上手 —— 上传接口的载体
- 12 · 错误处理日志配置 —— 413/415 错误体
- 16 · 服务端安全纵深 —— 上传是攻击面
- 17 · 部署与 CI/CD —— 对象存储与无状态部署
其他域
- 10 · Web 性能优化 —— CDN 与静态资源思维