华为云 OBS 预签名 URL 的原理与实现:为什么你的链接总是 403
预签名 URL 到底在签什么
很多人第一次用 OBS 预签名 URL,都会遇到一个反直觉的现象:本地用 SDK 生成链接一切正常,但把生成逻辑换成自己手写的 HTTP 请求,就稳定返回 403 SignatureDoesNotMatch。原因不在密钥,而在于签名串(string to sign)的拼装顺序和编码方式,SDK 帮你做了、手写代码漏了。
预签名 URL 的本质是:把一次 HTTP 请求的要素提前固定下来,用长期密钥算出一个签名,让不持有密钥的人也能在有效期内发起这一次请求。签名覆盖的要素越多,安全性越好,但任何一个要素在生成端和发送端不一致,校验就会失败。
V4 签名链路的四段结构
OBS 目前主推的是 Amazon S3 V4 签名体系,它的计算是一条单向链:
canonicalRequest = HTTP方法 \n 规范化URI \n 规范化QueryString \n
规范化Headers \n 签名Header列表 \n 空行
stringToSign = "AWS4-HMAC-SHA256" \n 时间戳 \n 凭证范围 \n
SHA256(canonicalRequest)
signingKey = HMAC 链(日期 -> 区域 -> 服务名 -> "aws4_request")
signature = HMAC-SHA256(signingKey, stringToSign) 的十六进制
四个步骤里,出错概率最高的是第一步的「规范化」。它有几条硬性规则:
- URI 要做 URL 编码,但斜杠
/不编码; - QueryString 的键和值都必须各自编码一次,然后按键名 ASCII 升序排列;
- Header 名转小写、去掉首尾空格、把连续空格压成一个;
- 参与签名的 Header 必须同时在请求里原样出现,否则服务端重建的签名串和你算的不一样。
用标准库实现一次
下面这段代码只依赖 hashlib、hmac 和 urllib.parse,不装 SDK 也能生成可用的 GET 预签名链接:
import hashlib, hmac, datetime, urllib.parse
AK = "你的AK"
SK = "你的SK"
BUCKET = "my-bucket"
HOST = f"{BUCKET}.obs.cn-north-4.myhuaweicloud.com"
REGION = "cn-north-4"
SERVICE = "s3"
def _hmac(key, msg):
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
def presign(object_key, expires=3600):
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
scope = f"{date_stamp}/{REGION}/{SERVICE}/aws4_request"
# 关键:斜杠保留,其余按 RFC3986 编码
uri = "/" + urllib.parse.quote(object_key, safe="/-_.~")
q = {
"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
"X-Amz-Credential": f"{AK}/{scope}",
"X-Amz-Date": amz_date,
"X-Amz-Expires": str(expires),
"X-Amz-SignedHeaders": "host",
}
qs = "&".join(
f"{urllib.parse.quote(k, safe='-_.~')}={urllib.parse.quote(str(v), safe='-_.~')}"
for k, v in sorted(q.items())
)
canonical = "\n".join([
"GET", uri, qs,
f"host:{HOST}\n",
"host",
"UNSIGNED-PAYLOAD",
])
sts = "\n".join([
"AWS4-HMAC-SHA256", amz_date, scope,
hashlib.sha256(canonical.encode("utf-8")).hexdigest(),
])
k = _hmac(("AWS4" + SK).encode("utf-8"), date_stamp)
k = _hmac(k, REGION)
k = _hmac(k, SERVICE)
k = _hmac(k, "aws4_request")
sig = hmac.new(k, sts.encode("utf-8"), hashlib.sha256).hexdigest()
return f"https://{HOST}{uri}?{qs}&X-Amz-Signature={sig}"
三个反复出现的坑
1. 域名写错,签名必然不匹配
签名串里签的是 host 这个 Header 的值。如果你生成签名时用的是虚拟主机风格 bucket.obs.region.myhuaweicloud.com,实际访问却换成了路径风格 obs.region.myhuaweicloud.com/bucket/...,host 变了,签名一定对不上。两种风格选一种,全程不要混用。
2. 时间戳与服务器偏差过大
V4 签名对时间敏感,请求的 X-Amz-Date 与服务器时间偏差超过 15 分钟就会直接报 RequestTimeTooSkewed。容器环境里时钟漂移是常态,建议每一批签名前重新取 utcnow(),不要缓存时间戳复用。
3. 有效期不是想设多长就多长
X-Amz-Expires 有上限约束,且它是「从 X-Amz-Date 起算」的秒数,不是绝对时间。想给用户一个「明天到期」的下载链接,正确做法是让 expires 落在允许范围内,过期后重新签发一次,而不是把秒数设成几万。
什么时候该用预签名,什么时候不该
预签名适合单次、短期、精确到具体对象的访问授权:把私有对象临时分享给外部、给前端直传提供上传入口。它不适合当作长期的访问控制手段——链接一旦泄露,在有效期内谁都能用,且无法单条吊销。
如果需要「可吊销、可审计、带条件限制」的访问,应该走 IAM 临时凭证(STS)换取临时 AK/SK,再在服务端代理转发,把权限判定放在你自己的业务层。预签名解决的是「临时放行」,不是「权限体系」。
小结
预签名 URL 的失败几乎都能归到同一类原因:生成端与校验端对同一份请求的理解不一致。排错时不要盯着密钥反复确认,直接把 canonicalRequest 打印出来逐行比对,问题通常在第 2 行(URI 编码)或第 3 行(QueryString 排序)上。
- 点赞
- 收藏
- 关注作者
评论(0)