web文件上传
浏览器里点击“上传”按钮,文件并不是直接飞进服务器的。它得先被切成数据块,塞进HTTP请求的body里,再沿着TCP连接一截一截爬过去。这个过程比多数人想象的要笨拙得多——尤其当你传一个几百兆的视频时,进度条卡在99%那种焦灼感,就是它在吭哧吭哧搬砖。 早年的做法更粗暴。1995年RFC 1867定义了 我见过一个老系统,上传大文件时内存暴涨。原因在于它用 现代浏览器提供了更细粒度的控制。File API让JavaScript能读取文件切片,XMLHttpRequest2支持上传进度事件,于是分片上传成了标配。把10GB文件切成5MB的块,逐块发送,服务端按序号拼接。断网了?记录已传的块索引,恢复后从断点继续。这套机制在网盘类产品里几乎是标配,但实现细节差异很大。有的产品分片大小固定,有的根据网络状况动态调整——后者体验明显更好,弱网环境下小分片重传成本低。 另一个容易被忽视的点是 存储环节也有讲究。直接存本地文件系统,简单但扩展性差;存对象存储(S3、OSS),得处理分片上传的ETag合并;存数据库?除非文件极小且量少,否则BLOB字段会让备份和查询都痛不欲生。我倾向于把元数据(文件名、大小、哈希、上传时间)放数据库,实体文件扔对象存储,用URL引用。这样迁移和扩容都灵活。 安全方面,文件名必须重写。用户传个 最后说个观察:很多团队把上传当作“简单功能”扔给新人练手,结果线上事故频发。其实它横跨网络、IO、安全、存储多个领域,每个环节都有暗礁。下次你点下上传按钮时,不妨想想背后这条数据链路——它比看上去要脆弱,也比看上去要有趣。multipart/form-data,让表单能携带二进制文件。原理简单:把文件内容按边界字符串分隔,拼成一段MIME格式的文本流。浏览器构造请求时,每个文件字段前面加一行Content-Disposition: form-data; name="file"; filename="photo.jpg",后面跟Content-Type声明类型,然后才是原始字节。服务器端解析这段流,按边界切出文件,写到磁盘。听起来利落,但实际写起来全是坑——边界字符串得保证在文件内容里不出现,否则解析直接崩掉。request.getParameter()读文件字段,容器默认把整个请求体缓存在内存里。一个2GB的ISO文件,直接把JVM堆撑爆。后来改成Apache Commons FileUpload的流式API,边读边写临时文件,内存占用才降到几十MB。这个教训很典型:上传的核心矛盾永远是内存与磁盘的权衡。Content-Type的欺骗性。用户上传一个.jpg,浏览器根据扩展名填image/jpeg,但文件头可能是PK\x03\x04——那是个zip压缩包。服务端如果只信这个字段,把文件当图片处理,轻则解析失败,重则触发ImageMagick之类的库漏洞。稳妥做法是读取文件魔数(magic number)二次校验。我参与过一个项目,攻击者上传伪装成头像的PHP脚本,靠的就是服务端没做内容嗅探,最终拿到了webshell。../../etc/passwd,如果服务端直接拼路径,那就是目录穿越。生成UUID作为存储名,原始文件名只存数据库供展示,能规避大部分路径注入。还有上传目录的执行权限——Web服务器绝不该在那里解析脚本。Nginx配个location ~* \.(php|jsp)$ { deny all; },成本极低,收益极高。
-
上一篇
-
下一篇