文件存储架构:对象存储、CDN 与直传
IronBench
5 分钟阅读
判断:用户上传文件若全部经应用服务器中转,服务器带宽和内存会成为瓶颈,且无法水平扩展。正确做法是"上传走直传(预签名 URL),下载走 CDN",应用服务器只负责签发凭证和记录元数据,不当文件搬运工。
为什么不要服务器中转
文件动辄几 MB 到几 GB,若走"客户端 → 应用服务器 → 对象存储",应用服务器既要收完整流又要转发,单机带宽和内存有限,并发一高就被拖垮。更糟的是长连接占用 worker,挤占正常 API。把文件流卸载到对象存储,应用服务器只处理轻量的"签发 URL"和"记元数据",横向扩容毫无压力。
直传:预签名 URL
对象存储(如 S3 兼容服务)支持服务端用密钥签发一个带过期时间的上传 URL,客户端拿这个 URL 直接 PUT 文件到存储,绕开应用服务器:
客户端请求 → 应用服务器校验权限
→ 生成预签名 PUT URL(有效期 5 分钟,限定 key 和大小)
→ 客户端直接 PUT 到对象存储
→ 存储返回成功,客户端通知应用服务器"上传完成"
预签名 URL 要限定对象 key、Content-Type、最大大小、过期时间,防止被拿去传任意内容或长期有效被滥用。
上传完整性校验
直传绕开了应用服务器,文件内容的合法性必须后置校验。存储侧可开启服务端校验(如 S3 的 Content-MD5 / 分片 ETag),客户端上传时携带哈希,存储端比对不一致则拒收。应用服务器在"上传完成"回调里再按真实文件头(magic number)校验类型,防止把 exe 改名 jpg 绕过。
大文件分片与续传
单次上传超时风险高,断网要从头再来。大文件应分块上传(如每块 5~10 MB),每块独立带预签名地址,失败只重传该块;全部块上传后由服务端按序合并。分块天然支持断点续传:已传块记录状态,恢复后只传剩余块。秒传则是对文件内容算哈希,若存储里已有相同哈希文件,直接复用不重复上传,省带宽。
下载走 CDN
文件读多写少,下载请求应走 CDN 边缘缓存。对象存储作为源站,CDN 把热门文件缓存到离用户近的节点,回源压力大幅下降。对私有文件,CDN 上同样用签名 URL 控制访问时效,并配置防盗链(Referer 白名单)避免被他人盗用流量。
元数据与文件本体分离
文件本身存对象存储,文件名用 UUID 或内容哈希避免冲突;文件的业务信息(归属用户、原始名、大小、类型)存在数据库。删除文件时既要删存储对象也要删元数据,否则出现"数据库说有、存储里没了"或反之。存储对象可配生命周期规则:热文件留标准存储,长期不访问的沉到冷存储,控制成本。
私有 bucket 的权限与防盗链
对象存储的 bucket 应默认私有,公开读只在确需场景下开启,且最好配合 CDN 签名 URL 控时效。预签名 URL 解决了"谁有权上传或下载",但 bucket 策略还要限制来源(如仅允许特定网络或账号),避免凭证泄露后被横向利用。下载侧的防盗链(来源白名单加空来源拒绝)能挡住被人直接盗用资源链接刷流量的行为。
大文件直传后,合并操作建议放在对象存储服务端(如分段上传的合并接口),而不是把分片拉回应用服务器拼装——那样又回到中转的老路。合并完成再回写元数据,保证"上传完成"回调里的文件是完整可用的。
上传成功后的二次校验时机
"上传完成"回调里做类型与大小校验,要在文件对业务可见之前完成。若先标记可见再异步校验,期间恶意文件可能已被下载。校验失败要能删除对象并回滚元数据,且通知上传方失败原因,不能静默丢弃让用户以为成功。
踩坑清单
只校验客户端声明的文件类型:攻击者把 exe 改名 .jpg 上传。服务端应在接收后按文件头或解析内容校验真实类型,再决定是否公开访问。
大文件不分片:单次上传超时风险高,断网要从头再来。应分块上传,每块独立重试,再由服务端合并。
文件名用用户原始名:中文、特殊字符、重名都会出问题。存储侧用生成的 key,原始名仅作展示字段。
CDN 缓存了错误响应:源站 404 被缓存,文件其实已存在。配置 CDN 不为 4xx/5xx 缓存,或上传完成后再刷新缓存。
预签名 URL 不过期或过长:被截取后可长期滥用。短有效期 + 用后即废更安全。
浏览 0
点赞 0
评论 0