12 分钟
全栈开发

文件上传与静态资源

multipart 上传、大小类型限制、存储与 URL 暴露的完整往返

  • 理解 multipart/form-data 为什么不能用 JSON 传文件
  • 掌握上传时的大小与类型限制
  • 能识别路径穿越与类型伪造两类上传漏洞
  • 了解存本地磁盘 vs 对象存储 + CDN 的取舍
💡高中搭桥:像收快递

前台收包裹(接收上传字节)→ 扫码存进储物间(落盘或对象存储)→ 给你一个取件码(返回访问 URL)。文件上传接口就是这条流水线,少一环都拿不到图。

文件为什么不走 JSON?

JSON 只能塞文本,二进制图片直接塞进去会体积膨胀三倍还容易乱码。上传文件要带文件名、文件类型、二进制内容,HTTP 用 multipart/form-data 这个格式把它们分段打包。

ℹ️两种请求体对照

application/json:传结构化文本,登录、增删改查都用它。multipart/form-data:传文件 + 字段,头像、附件、图片上传用它。选择口诀:带二进制文件就用 multipart,其余用 JSON。

上传校验:大小与类型

开放上传等于开放别人往你服务器塞东西,必须限制:大小上限(比如头像 2MB)类型白名单(只允许 png/jpg/gif)别信前端传的后缀,后端按字节内容复核。下面用纯 Python 模拟接收与校验。记住这三条都必须在服务端做,前端校验只是给正常用户省一次请求。

示例代码(可运行)

上传漏洞:别信前端说的任何话

开放上传接口是攻击者最爱打的入口之一,因为它直接决定别人能往你服务器写什么文件。两个经典漏洞必须知道。

路径穿越(directory traversal):前端把文件名传成 ../../../var/www/evil.php,后端图省事直接拼接路径落盘,文件就被写到了网站根目录,等于对方在你服务器上种了一个 WebShell,之后能远程执行命令。正确做法:永远不要用用户传来的原始文件名拼磁盘路径,自己生成一个随机文件名(上面代码里 avatar_随机数.ext 就是这个思路),落盘目录和 Web 根目录严格分开。

文件类型伪造:攻击者把一个恶意 .php 文件后缀改成 .png 上传,后端只看后缀就放行,文件被解析执行。正确做法:除了看后缀白名单,还要读文件头几个字节(魔数)判断真实类型——PNG 文件头固定是一串魔数,伪造后缀改不了它。生产上再叠一层异步病毒扫描兜底。

还有一条铁律:前端校验只是体验,不是安全。浏览器里的『禁止选择 .exe』随便用 Postman 就能绕过,所有大小、类型、数量限制必须在后端再做一遍。很多事故都是『前端禁了、后端忘了拦』。另外别用用户原始文件名落盘——两个人同时上传同名头像会互相覆盖,自己生成随机名还顺手解决了这个问题。

🚫上传三道闸

第一道:自己生成文件名,绝不用用户原始名拼路径(防路径穿越)。第二道:读文件魔数 + 后缀白名单,不信前端声明的类型(防类型伪造)。第三道:上传后异步病毒扫描,可疑文件隔离不对外暴露。隐私文件(合同、证件)别放公开读的桶,要用带过期时间的签名 URL,下载完即失效。三道闸各管一段,缺一不可。

存哪?怎么暴露?

存本地磁盘:小项目最快,后端把 uploads 目录挂成静态路由,通过 /static/xxx 直接访问。但多台服务器之间文件不共享,服务器重装文件就没了。

对象存储(如腾讯云 COS、阿里云 OSS,价格与额度以官方最新为准):文件传上去就给一个公网 URL,配合 CDN 分发。生产环境头像、附件基本都走对象存储。

示例
# 完整往返(静态示意)
# 1. 前端:<input type="file"> 选头像,FormData 提交
# 2. 后端:接收字节 → 校验大小类型 → 存盘/传对象存储
# 3. 后端:返回 { "url": "https://cdn.xxx.com/avatar_123.png" }
# 4. 前端:<img src={url}> 直接显示

大文件分片与 CDN 回源

传一个 2GB 的视频,一次性上传中途断网就得从头再来。分片上传思路:前端把文件切成 5MB 一小块一小块传,每片带序号和总片数,后端收齐后按序号合并;中途断了只重传缺的那几片,最后一片传完触发合并。这是网盘、视频站的标配。

对象存储 + CDN 配合的回源逻辑:文件传到 COS/OSS 后,公网 URL 前面套一层 CDN。用户访问图片时先找离他最近的 CDN 边缘节点,有缓存直接返回;没有才「回源」去对象存储拉一次并缓存下来。这样热门头像被成千上万人看,压力全在 CDN 上,你自己的源站带宽几乎不花钱。

头像这种小图还有个工程细节:前端选完图先在浏览器里压缩缩放到合适尺寸再上传,能把一张 10MB 的手机照片压到 200KB 以内,上传快、存储省、加载也快。别让用户原图直传——那是对两边带宽的浪费。分片上传同理,传完一片就记一下进度,刷新页面能从断点继续,不用从头来。另外每个用户最好设每日上传配额,防止有人用你的存储当免费网盘。

🐍这一课为 m5 铺路

下一个全栈毕业项目要做用户头像上传,这一节就是它的零件。真实 FastAPI 里用 UploadFile 接收,逻辑和上面模拟的完全一样。

选择题

前端上传用户头像时,请求头的 Content-Type 应该是?

选择题

攻击者上传文件名 ../../../var/www/evil.php,后端直接拼接落盘导致 WebShell。这属于哪种漏洞?

填空题填写空白处的代码
文件存好后,后端返回一个 ,前端用 <img src> 把它显示出来。

资深工程师加餐

底层原理 · 大厂视角 · 工程经验,点卡片展开

穿透是反复查「根本不存在」的数据,用布隆过滤器或缓存空值挡;击穿是某个热点 key 过期瞬间大量请求压向数据库,用互斥重建或逻辑过期;雪崩是大批 key 同时过期,给 TTL 加随机抖动。缓存与库的一致性常用「先更新数据库、再删除缓存」,并靠重试/订阅补偿。

挑战任务

文件超限闸门

简单+50 XP

场景:你的上传接口经常被人传几个 GB 的垃圾文件打爆。先写 check_size(size, limit) 拦住超大数据——size 不超过 limit 放行返回 True,否则拒绝返回 False。

文件超限闸门
1 个测试用例

课后作业

设计头像上传规则

中等+20 XP

场景:你的头像上传接口被人传过后门文件。请列出你的四道规则——大小上限、允许格式、存储位置(本地还是对象存储)、以及防路径穿越/类型伪造各怎么落地。

设计头像上传规则
1 个测试用例