对象存储预签名 URL 的三个高频失效场景
把私有桶里的对象临时开放给前端或第三方下载,预签名 URL 是最常见的做法:服务端用 AK/SK 生成一个带签名的链接,带过期时间,谁拿到链接谁就能在有效期内访问,不需要暴露密钥。
看起来简单,但它有几种典型的失效方式,排查起来都不太直观。下面三个场景是我们实际踩过的。
一、时间偏差:签名"刚生成就过期"
预签名 URL 的签名串里包含 `Expires`(或 `X-Amz-Date`),服务端校验时会拿它和**自己的时钟**比较。如果生成签名的机器时间比对象存储服务快了几分钟,就会出现很诡异的现象:链接在浏览器里打开立即报签名过期,而代码里明明设置了一小时有效期。
这类问题的排查要点是先对齐时钟:
# 看本机与公共 NTP 的偏差
chronyc tracking | grep -E "System time|Last offset"
# 或者直接对比 HTTP 响应头里的 Date
curl -sI https://obs.example.com | grep -i '^date'
容器环境尤其容易踩:宿主机做了时间同步,容器镜像里的 tzdata 缺失或 chrony 没跑起来,容器时钟慢慢漂移。规范做法是让宿主机负责时间,容器只管时区(`TZ` 环境变量),不要在容器内再起一个时间同步进程。
二、路径编码:中文名和加号最容易出事
签名是对"规范化后的资源路径"计算的。如果服务端签名时用的 key 是 `docs/季度报告.pdf`,而实际请求时浏览器把它编码成了 `docs/%E5%AD%A3%E5%BA%A6%E6%8A%A5%E5%91%8A.pdf`,两边的规范化结果不一致,签名就对不上,返回 403 SignatureDoesNotMatch。
几个容易忽略的细节:
- 空格在 URL 里应该编码成 `%20`,而不是 `+`。`+` 是 `application/x-www-form-urlencoded` 的约定,在路径段里它就是一个普通字符。
- 加号本身如果出现在文件名里,必须编码成 `%2B`,否则会被某些网关还原成空格。
- 生成签名后不要再用另一套 URL 编码函数"美化"一遍,SDK 返回的 URL 直接用。
经验做法是:上传时就限制 key 只允许 `[a-zA-Z0-9._/-]`,从源头避免编码歧义。确实需要中文名时,统一在服务端做一次 `encodeURIComponent` 后再签名,并且前后端都用同一个编码函数。
三、请求头与签名绑定的那部分
很多 SDK 允许把 `Content-Type`、`Content-Disposition` 之类放进签名。一旦签进去了,客户端请求时必须**完全一致**地带同样的头,否则签名校验失败。
最典型的坑是浏览器直传:服务端按 `Content-Type: image/png` 签了 URL,前端用 `fetch` 上传时浏览器自动加了个 `Content-Type: image/png; charset=utf-8`,或者 `multipart/form-data; boundary=...`,于是 403。这类问题在 curl 里复现不出来,因为 curl 不会自动加。
排查方式是把请求头原样打出来比对:
curl -v -X PUT "$URL" -H "Content-Type: image/png" --data-binary @a.png 2>&1 | grep -E '^> '
如果确定不需要限制类型,最好在签名阶段就不要包含它,签名串里越少东西越稳。
附带的两个实践建议
**有效期不要给太长。** 预签名 URL 一旦泄露,在过期前任何人都能用。给前端下载的场景,十分钟到一小时足够;需要长时间访问的,走服务端代理或用临时凭证(STS)换取有时效的访问权限,而不是签一个七天有效的链接。
**出问题先看返回体的错误码,不要猜。** 对象存储的 403 通常带 `Code` 字段:`SignatureDoesNotMatch` 是签名串不一致(编码、头、时间三者之一),`AccessDenied` 是权限策略不允许,`RequestTimeTooSkewed` 是时钟偏差。这三个错误的修复方向完全不同,凭猜只会来回改代码。
小结
预签名 URL 的失效几乎都能归到一句话:**签名时用什么,请求时就得一模一样。** 时间、编码、请求头,三处任一处不一致,结果都是 403。把这三项在联调阶段固化成检查清单,比事后对着日志猜要快得多。
- 点赞
- 收藏
- 关注作者
评论(0)