LabHub
学习 学习路径 课程

对象存储与 S3

预签名 URL 解决的问题

在 LabHub 中继续学习

一句话总结

预签名 URL 用一个签过名的 URL 表达“此人可以在这段时间内,只对这个对象执行这一项操作”。字节流无需再经过应用服务器。

流程图: CORS。 · 签名涵盖的请求头必须原样发送。 · 时钟。 · 应用程序自身无法知道上传已经完成。

为什么需要了解这些

让用户下载其上传文件的最朴素实现方式如下:应用程序检查权限,从存储中读取对象,再通过响应正文把内容传出去。

这种方式的问题会在规模扩大时显现。如果同时下载 100 个 500MB 的视频,应用服务器就要中转 50GB 数据。工作进程会一直被占用,带宽受限于应用服务器规格;内存缓冲一旦处理不当,还会触发 OOM。应用服务器应当处理业务逻辑,而不应沦为搬运字节的管道。

预签名 URL 对职责进行了拆分。应用负责判断权限、签署 URL 并返回;真正的字节传输直接发生在客户端与存储之间。应用只需生成一个占用几字节的 URL。

工作原理

URL 在包含签名的同时也带有约束条件:允许执行什么操作(GET/PUT)、针对哪个对象、有效到何时(X-Amz-Expires)、使用哪项凭据(X-Amz-Credential),以及签名涵盖了哪些请求头(X-Amz-SignedHeaders)。存储系统验证签名后决定是否允许请求。关键在于,签名虽然由凭据生成,但 URL 中并不包含秘密密钥本身。

它也可用于上传。向客户端提供预签名 PUT URL 后,客户端便可直接上传到存储。此时需要注意限制客户端能上传什么以及上传多少。使用预签名 POST 策略,可以把 Content-Length 范围和 Content-Type 一并纳入签名约束。

有效期越短越好。下载 URL 通常几分钟、上传 URL 通常几十分钟就已足够,因为 URL 可能从日志或 Referer 请求头中泄漏。反过来,如果有效期过短,网络较慢的用户可能无法完成上传。

策略属于另一个层级。如果说预签名是临时委派,那么策略就是长期权限。它使用 IAM 风格的 JSON 描述哪个主体可以对哪些资源执行哪些操作。常见错误是把 Resource 设为整个存储桶。若将范围缩小到前缀级别,即便某个服务被攻破,损害也会被限制在该前缀内。

生产现场会看到什么

允许匿名读取整个存储桶,是大多数数据泄漏事故的起点。人们常因“里面只有图片”而开放访问,之后备份或用户上传内容却又混入其中。如果确实需要匿名公开,不应开放整个存储桶,而应只开放 public/ 这样的特定前缀,并通过代码强制规定该前缀只能放置可公开内容。

此外,现实中确实会出现 MinIO CVE-2025-62506 这类通过绕过会话策略提升权限的漏洞。收紧策略也是对实现缺陷的一层防御。

从浏览器直接上传时会遇到的问题

第一次接入预签名 URL 时,通常都会卡在相同环节。下面按顺序逐一说明。

CORS。 浏览器要向不同源的存储发送 PUT,首先会发送 OPTIONS 预检请求,存储必须在响应中声明允许的请求头。这项配置需要单独添加到存储桶;缺少配置时,签名本身完全正常,只有浏览器控制台会显示 CORS 错误。因为从服务器使用 curl 测试时一切正常,很容易在错误方向上排查原因。

签名涵盖的请求头必须原样发送。 如果签名中包含 Content-Type,客户端就必须发送完全相同的值,任何差异都会导致 SignatureDoesNotMatch。浏览器或 HTTP 库有时会自行添加请求头或改变大小写,因此只把确有必要的请求头纳入签名更安全。

时钟。 签名包含签发时间,而存储会用自己的时钟验证。如果两边时钟相差几分钟以上,刚生成的 URL 就会因“已过期”而被拒绝。在容器没有 NTP,或笔记本电脑刚从睡眠状态唤醒时,确实会遇到这种问题。

上传完成后的处理方式也必须预先确定。由于客户端直接把内容上传到了存储,应用程序自身无法知道上传已经完成。 让客户端发送“上传完成”通知的做法,会在该请求未送达时彻底中断流程。因此实际工作中会同时采用两种机制:通过存储事件通知接收对象创建事件并进行处理,同时定期扫描并清理在一定时间后仍未完成的条目。

大文件不会一次性上传。分段上传会把文件拆成小块并行上传,最后再合并;即使中途断开,也只需重传失败的小块。但与此同时,没有调用合并操作的分段会一直残留并产生费用。 它们不会出现在可见的对象列表中,往往要到几个月后发现容量对不上时才暴露。标准做法是在创建存储桶时,同时添加一条生命周期规则,在数天后自动删除未完成的上传。

下一次实验要做什么

先确认匿名访问会被拒绝,再生成并使用预签名 GET 和 PUT URL,并验证过期行为。随后在另一个实验中创建用户,编写并挂载只读策略和限定前缀的策略。