华为云 OBS 预签名 URL 的原理与实现:为什么你的链接总是 403

举报
yd_267437536 发表于 2026/10/27 18:01:52 2026/10/27
【摘要】 预签名 URL 看着简单,但签名串里每个字段的处理都能决定成败。这篇把 V4 签名的构造过程拆开,附一段只用标准库的 Python 实现。华为云OBS

预签名 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 排序)上。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。