华为云 OBS 私有桶 + CDN 加速:从 SignatureDoesNotMatch 到彻底解决

📅 2026/7/29 6:26:10 👁️ 阅读次数 📝 编程学习
华为云 OBS 私有桶 + CDN 加速:从 SignatureDoesNotMatch 到彻底解决

华为云 OBS 私有桶 + CDN 加速:从 SignatureDoesNotMatch 到彻底解决

一个SignatureDoesNotMatch错误,牵出 S3 签名协议三代演进、CDN 回源 Host 陷阱、以及私有桶分发的最佳实践。本文从一次真实的线上故障出发,逐层剥开问题,最终给出生产级解决方案。


目录

  • 一、问题现场
  • 二、第一反应:是不是 AK/SK 错了?
  • 三、根因定位:签名 Host ≠ 验签 Host
  • 四、签名协议全景:V2 → V4 → V4A
  • 五、为什么"短 URL"能用、"长 URL"不能用
  • 六、三条解决路线
  • 七、推荐方案:CDN URL 鉴权 + OBS 回源鉴权
  • 八、完整代码
  • 九、控制台配置清单
  • 十、踩坑备忘
  • 十一、总结

一、问题现场

业务架构很常见:

用户浏览器 │ ▼ CDN 加速域名(oss.xxx.com) │ 回源 ▼ 华为云 OBS 私有桶(obs.cn-north-4.myhuaweicloud.com)

用 boto3 生成预签名 URL,把 host 替换成加速域名后给用户下载:

importboto3frombotocore.configimportConfig client=boto3.client('s3',endpoint_url='https://obs.cn-north-4.myhuaweicloud.com',region_name='cn-north-4',aws_access_key_id=AK,aws_secret_access_key=SK,config=Config(signature_version='s3v4',s3={'addressing_style':'path'}),)url=client.generate_presigned_url('get_object',Params={'Bucket':'PROD-A2E456','Key':'20260728/report.png'},ExpiresIn=3600,)# "聪明"地把 host 换成加速域名url=url.replace('obs.cn-north-4.myhuaweicloud.com','oss.xxx.com')

浏览器访问,403

<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature does not match.</Message></Error>

奇怪的是,同事从 OBS 控制台"分享"出来的 URL 却能正常访问

https://oss.xxx.com/PROD-A2E456/20260728/report.png ?AWSAccessKeyId=C6VJHSSFEPDKXFE72AL5 &Expires=1785229850 &Signature=9gfeDm5QQFJZo2pGZ%2F7j7MVR204%3D

只有 3 个参数,签名才 28 个字符。而我们 Python 生成的 URL 有 6 个参数,签名 64 个字符,还不能用。

为什么?


二、第一反应:是不是 AK/SK 错了?

先排除最基本的错误。写一个直连 OBS(不经 CDN)的自证脚本:

client.head_object(Bucket='PROD-A2E456',Key='20260728/report.png')
  • 成功 → AK/SK/桶名/Key/Region 全对,问题 100% 出在 CDN 那一跳。
  • 失败 → 先修凭证,后面的都不用看。

我们的结果是成功。那问题就锁定在:请求经过 CDN 后,OBS 验签失败。


三、根因定位:签名 Host ≠ 验签 Host

3.1 请求链路中 Host 发生了什么

① boto3 签名时: endpoint_url = obs.cn-north-4.myhuaweicloud.com → 签名输入里 host = obs.cn-north-4.myhuaweicloud.com ② 我们 replace 后: URL host = oss.xxx.com ③ 浏览器请求 CDN: Host 头 = oss.xxx.com ④ CDN 回源 OBS: 回源 HOST = ???(取决于 CDN 控制台配置) 默认行为:回源 HOST = 源站地址 = obs.cn-north-4.myhuaweicloud.com (或者被我们 replace 后的 oss.xxx.com,取决于配置) ⑤ OBS 验签: 用收到的 Host 头重建 Canonical Request → 算 SHA256 → 与签名比对

只要 ① 和 ⑤ 的 host 不一致,签名就废了。

3.2 为什么 V4 签名里 host 这么重要

V4 的 Canonical Request 长这样:

GET /PROD-A2E456/20260728/report.png X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-SignedHeaders=host host:oss.xxx.com ← 这一行参与了 SHA256 计算! host UNSIGNED-PAYLOAD

X-Amz-SignedHeaders=host明确声明:host 是签名的一部分。

OBS 收到请求后,用实际收到的 Host 头重建这段文本。如果 CDN 回源时把 Host 改成了obs.cn-north-4.myhuaweicloud.com,那重建出来的文本就和签名时不同 → SHA256 不同 → 签名对不上 → 403。

3.3 一句话根因

V4 签名把 host 签进去了。你在客户端用真实地址签名,然后把 URL 的 host 文本替换成加速域名(或 CDN 回源时改了 Host),导致 OBS 验签时重建的 Canonical Request 与签名时不一致。


四、签名协议全景:V2 → V4 → V4A

要理解为什么控制台那条"短 URL"能用,必须搞清楚 S3/OBS 签名的三个版本。

4.1 总览

V2(Legacy)V4(SigV4)V4A(SigV4a)
发布年份~200620112022
哈希算法HMAC‑SHA1HMAC‑SHA256ECDSA P‑256 + SHA256
签名输出20B → Base64 →28 字符32B → Hex →64 字符DER → Base64 →~72 字符
签 Host?不签✅ 签✅ 签
签 Region?✅(可写*
密钥派生直接用 SK4 层 HMAC 派生从 SK 派生 ECDSA 私钥
AWS 状态已废弃当前标准仅 MRAP 场景
华为云 OBS✅ 仍支持✅ 仍支持❌ 不支持
boto3 配置signature_version='s3'signature_version='s3v4'signature_version='s3v4a'

4.2 V2 签名详解

StringToSign:

GET\n \n \n 1785229850\n /PROD-A2E456/20260728/report.png

没有 host、没有 region、没有 service。签名计算:

importhmac,hashlib,base64 string_to_sign="GET\n\n\n1785229850\n/PROD-A2E456/20260728/report.png"signature=base64.b64encode(hmac.new(SK.encode(),string_to_sign.encode(),hashlib.sha1).digest()).decode()# → "9gfeDm5QQFJZo2pGZ/7j7MVR204="

预签名 URL 只有 3 个参数:

?AWSAccessKeyId=C6VJHSSFEPDKXFE72AL5 &Expires=1785229850 &Signature=9gfeDm5QQFJZo2pGZ%2F7j7MVR204%3D

4.3 V4 签名详解

四步流程:

① Canonical Request(含 host) ② StringToSign(含 CredentialScope: date/region/service) ③ Signing Key = HMAC(HMAC(HMAC(HMAC("AWS4"+SK, date), region), service), "aws4_request") ④ Signature = HexHMAC(SigningKey, StringToSign)

预签名 URL 有 6 个参数:

?X-Amz-Algorithm=AWS4-HMAC-SHA256 &X-Amz-Credential=C6VJ.../20260728/cn-north-4/s3/aws4_request &X-Amz-Date=20260728T094800Z &X-Amz-Expires=3600 &X-Amz-SignedHeaders=host &X-Amz-Signature=9b4483004584032cc1c9091cb987affd3f84e5d5b4b86f9bc42bf3858950775f

4.4 V4A 签名(了解即可)

V4A 用ECDSA 非对称签名解决 Multi-Region Access Point 的跨 region 验签问题。CredentialScope 里 region 写*,任何 region 的节点都能用公钥验签。

华为云 OBS 不支持 V4A,在华为云场景下无需关心。

4.5 演进关系

2006 V2(SHA1,不签 host/region) │ 缺陷:SHA1 弱、不绑 host → 可重定向、不绑 region → 可跨区重放 ▼ 2011 V4(SHA256,签 host + region + service + date,4 层密钥派生) │ 缺陷:签名绑定单个 region,无法跨区验签 ▼ 2022 V4A(ECDSA P-256,region=*,非对称,多区域通用)

V4A 的 Canonical Request 格式与 V4完全相同,只是算法标识和签名步骤不同。V2 与 V4 之间没有向后兼容


五、为什么"短 URL"能用、"长 URL"不能用

这是整件事的终极答案

V2(短 URL)V4(长 URL)
签名输入里有 host?没有
CDN 回源改 Host无所谓,host 不参与验签致命:host 变了 → SHA256 不同 → 403
能否安全 replace host?✅ 能(host 没被签)❌ 不能(host 被签了)
URL 参数3 个6 个
签名长度28 字符64 字符

一句话:V2 的"短"和"能用"是同一个原因——它不签 host。V4 的"长"和"不能用"也是同一个原因——它签了 host,而 CDN 改了 host。

那条控制台生成的短 URL,用的是 V2 签名(华为云 OBS 控制台"分享"功能默认 V2)。V2 的 StringToSign 里根本没有 host 这一行,所以无论 CDN 回源时把 Host 改成什么,OBS 验签时重建的 StringToSign 都和签名时一模一样。签名永远对得上。


六、三条解决路线

路线 A:Python 改用 V2 签名(最快,5 分钟)

config=Config(signature_version='s3',s3={'addressing_style':'path'})# 's3' = V2client=boto3.client('s3',endpoint_url='https://obs.cn-north-4.myhuaweicloud.com',...)url=client.generate_presigned_url('get_object',Params={...},ExpiresIn=3600)url=url.replace('obs.cn-north-4.myhuaweicloud.com','oss.xxx.com')# V2 下安全
  • ✅ 改动最小,立刻能用
  • ❌ SHA1 安全性弱,不绑 host/region,AWS 已废弃,华为云未来不保证

路线 B:坚持 V4,对齐 Host(需改 CDN 控制台)

config=Config(signature_version='s3v4',s3={'addressing_style':'path'})client=boto3.client('s3',endpoint_url='https://oss.xxx.com',...)# 加速域名!url=client.generate_presigned_url(...)# 不要 replace!

同时在 CDN 控制台把「回源 HOST」显式设为oss.cmrcglobal.com

  • ✅ V4 安全性完整
  • ❌ 必须改 CDN 控制台,若回源 HOST 被其他业务依赖则冲突

路线 C:CDN URL 鉴权 + OBS 回源鉴权(推荐,长期正解)

客户端不做 OBS 签名,改由 CDN 边缘节点做 URL 鉴权。

  • ✅ 彻底告别 Host 问题(OBS 签名根本不存在了)
  • ✅ URL 最短(只有一个auth_key参数)
  • ✅ 缓存友好(忽略auth_key后缓存 key = path)
  • ✅ 安全性不低于 V2(有时效 + 私有密钥)
  • ⚠️ 需要配置 CDN URL 鉴权 + OBS 回源鉴权(一次性工作)

七、推荐方案:CDN URL 鉴权 + OBS 回源鉴权

7.1 架构

用户 ──① CDN URL鉴权──► CDN边缘节点 ──② OBS回源鉴权──► OBS私有桶 (auth_key参数) (验证通过才放行) (委托AK/SK) (桶仍私有)
谁验验什么作用
① 用户→CDNCDN 边缘节点auth_key(时间戳+MD5)防盗链、防遍历、限时
② CDN→OBSOBSCDN 用委托凭证签名桶保持私有

OBS 桶仍然是私有的。公网直连obs.cn-north-4.myhuaweicloud.com/桶/key照样 403。用户必须带合法的auth_key才能从 CDN 拿到内容。

7.2 CDN URL 鉴权方式 A 原理

URL 格式:

https://oss.xxx.com/<key>?auth_key={timestamp}-{rand}-{uid}-{md5hash}
字段含义
timestampUnix 时间戳(十六进制)
rand随机串(可为0
uid用户标识(可为0
md5hashMD5("{uri}-{timestamp}-{rand}-{uid}-{PrivateKey}")

CDN 边缘节点收到请求后:

  1. 没有auth_key→ 403
  2. timestamp + 有效时长 < 当前时间→ 过期 403
  3. 用 PrivateKey 重算 MD5,对不上 → 403
  4. 全通过 → 放行(有缓存直接吐,没缓存才回源)

关键优势:验签在 CDN 边缘完成,不涉及 OBS、不涉及 Host 头,彻底告别SignatureDoesNotMatch

7.3 安全性

“放弃签名后,用户只要拼出 URL 就能访问?”

不是。

  • 不知道CDN_AUTH_KEY(PrivateKey)→ 算不出合法auth_key→ 拼不出有效 URL → 403
  • auth_key有时效(过期作废)
  • PrivateKey 只存在你的服务器和 CDN 控制台,不出现在 URL 里
  • OBS 桶仍私有,绕过 CDN 直连 OBS 仍 403

安全性对比:

方案安全等级Host 问题缓存友好
V4 预签名 + CDN★★★★❌ 改 Host 就崩❌ 6 个参数
V2 预签名 + CDN★★★✅ 无❌ 3 个参数
CDN URL 鉴权★★★★✅ 完全不涉及✅ 忽略后 key=path
裸 URL

八、完整代码

# -*- coding: utf-8 -*-""" 华为云 OBS 私有桶 + CDN 加速 + CDN URL鉴权(方式A) 完整示例 架构:用户 → CDN(auth_key鉴权) → OBS(回源鉴权/委托) → 私有桶 依赖:pip install boto3 requests 凭证:export HWC_AK=... ; export HWC_SK=... ; export CDN_AUTH_KEY=... """importosimportsysimporttimeimporthashlibimportloggingfromurllib.parseimportquoteimportboto3frombotocore.configimportConfig logging.basicConfig(level=logging.INFO,format="%(asctime)s %(levelname)s | %(message)s")log=logging.getLogger("obs-cdn")# ======================== 配置 ========================AK=os.environ.get("HWC_AK","")SK=os.environ.get("HWC_SK","")REGION="cn-north-4"OBS_ENDPOINT="https://obs.cn-north-4.myhuaweicloud.com"BUCKET="PROD-A2E456"ACCEL_DOMAIN="oss.xxx.com"CDN_AUTH_KEY=os.environ.get("CDN_AUTH_KEY","")# CDN控制台「URL鉴权」里的 PrivateKeyCDN_AUTH_TTL=3600# 有效时长(秒),与控制台一致# ======================== CDN URL 鉴权(方式 A)========================defcdn_auth_url(key:str,ttl:int=CDN_AUTH_TTL)->str:""" 生成带 CDN URL鉴权(方式A) 的访问链接。 格式: https://domain/key?auth_key={timestamp}-{rand}-{uid}-{md5hash} """timestamp=format(int(time.time()),"x")# 十六进制rand="0"uid="0"uri="/"+quote(key,safe="/")# MD5("{uri}-{timestamp}-{rand}-{uid}-{PrivateKey}")sign_str=f"{uri}-{timestamp}-{rand}-{uid}-{CDN_AUTH_KEY}"md5hash=hashlib.md5(sign_str.encode()).hexdigest()auth_key=f"{timestamp}-{rand}-{uid}-{md5hash}"returnf"https://{ACCEL_DOMAIN}{uri}?auth_key={auth_key}"# ======================== SDK 管理(直连 OBS)========================defget_mgmt_client():"""上传/删除/head 用。endpoint = 真实地址,直连 OBS,与 CDN 无关。"""returnboto3.client("s3",endpoint_url=OBS_ENDPOINT,region_name=REGION,aws_access_key_id=AK,aws_secret_access_key=SK,config=Config(signature_version="s3v4",s3={"addressing_style":"path"}),)defupload(key:str,body:bytes):get_mgmt_client().put_object(Bucket=BUCKET,Key=key,Body=body)log.info("上传成功: %s",key)defself_check(key:str):"""直连 head_object,自证凭证正确。"""get_mgmt_client().head_object(Bucket=BUCKET,Key=key)log.info("自证通过:直连 OBS 正常。")# ======================== 验证 ========================defverify(url:str,tag:str):importrequests r=requests.get(url,timeout=15)log.info("[%s] HTTP %d len=%d",tag,r.status_code,len(r.content))ifr.status_code==403:log.info("[%s] body: %s",tag,r.text[:300])# ======================== 主流程 ========================defmain():ifnotAKornotSKornotCDN_AUTH_KEY:log.error("请设置 HWC_AK / HWC_SK / CDN_AUTH_KEY 环境变量")sys.exit(1)key="20260728/PROD-A2E456-202607281654-202607281751-utc8.png"# 1. 上传 + 自证(可选,对象已存在则跳过)# upload(key, b"fake-png-data")self_check(key)# 2. 生成 CDN 鉴权 URLurl=cdn_auth_url(key)log.info("CDN鉴权URL:\n %s",url)verify(url,"cdn-auth")# 3. 对比:裸 URL(无 auth_key)→ 应被 CDN 403bare=f"https://{ACCEL_DOMAIN}/{quote(key,safe='/')}"verify(bare,"bare-should-403")if__name__=="__main__":main()

运行输出示例:

2026-07-28 17:52:01 INFO | 自证通过:直连 OBS 正常。 2026-07-28 17:52:01 INFO | CDN鉴权URL: https://oss.xxx.com/20260728/PROD-A2E456-...png?auth_key=6887a3fe-0-0-a1b2c3d4e5f6... 2026-07-28 17:52:02 INFO | [cdn-auth] HTTP 200 len=245760 2026-07-28 17:52:02 INFO | [bare-should-403] HTTP 403 len=0

九、控制台配置清单

#位置操作说明
1CDN → 域名 → 源站配置源站类型 = OBS 桶;开启OBS 回源鉴权+ 委托授权让 CDN 能读私有桶
2CDN → 域名 → 访问控制 →URL 鉴权方式 A;PrivateKey = 你的密钥;有效时长 = 3600s与代码里CDN_AUTH_KEY一致
3CDN → 域名 → 缓存配置忽略auth_key参数(或忽略全部 URL 参数)⚠️ 极易遗漏!否则缓存命中率归零
4OBS 桶 → 权限保持私有不要开公共读
5IAM → 委托创建 CDN → OBS 的委托(若控制台提示)一次性

步骤 3 是最容易踩的坑:CDN 默认把整个 query string 当缓存 key。auth_key每次都不同 → 每次都是新缓存 → 永远回源 → 流量费爆炸。必须忽略。


十、踩坑备忘

坑 1:对 V4 预签名 URL 做url.replace(host)

# ❌ 致命错误(V4 下)url=url.replace('obs.cn-north-4.myhuaweicloud.com','oss.cmrcglobal.com')

V4 签了 host,replace 后 host 与签名脱钩 → 必 403。

V2 下这个操作是安全的(host 没被签),但不要养成这个习惯

坑 2:往 URL 加随机参数"破缓存"

# ❌ 不要这么干url+="&_t="+str(time.time())

对 V4 预签名 URL:额外 query 参数会被纳入 Canonical Request → 签名 mismatch → 403。
对 CDN 鉴权 URL:如果没忽略参数,每个_t值都是独立缓存 → 命中率归零。

正确做法:在 CDN 控制台对指定 URL 做「刷新」。

坑 3:对 4xx 响应配了长缓存

CDN 缓存规则里如果403 → 缓存 300s,那第一次 403 后,5 分钟内所有请求都直接返回缓存的 403,你改了配置也看不到效果。

排查时:对该 URL 做一次「刷新」,或用全新 key 的对象测试。

坑 4:CDN 预热用了 OBS 真实地址

# ❌ 预热 URL 的 host 必须是加速域名 https://obs.cn-north-4.myhuaweicloud.com/CMRC-PROD-VT7J15/key.png → CDN.0108 错误 https://oss.xxx.com/PROD-A2E456/key.png → 正确

CDN 预热是告诉边缘节点"去缓存这个 URL",URL 的 host 必须是 CDN 加速域名,否则 CDN 不认识这个 URL → 报CDN.0108

坑 5:addressing_stylevirtual

华为云 OBS 用 boto3 时,如果用virtual风格,桶名会被放到 host 里(bucket.obs.cn-north-4...),导致 host 解析和签名更复杂。

统一用path风格:桶名在 path 里,host 只参与签名校验,逻辑最清晰。


十一、总结

问题本质

V4 签名绑定了 host + CDN 回源改了 host = SignatureDoesNotMatch

解决思路演进

阶段方案评价
应急V2 签名(signature_version='s3')+ replace host能用,但 SHA1 弱、不绑 host
过渡V4 签名 + endpoint 填加速域名 + CDN 回源 HOST 对齐正确但脆弱,依赖控制台配置
终态CDN URL 鉴权 + OBS 回源鉴权无 OBS 签名 → 无 Host 问题 → 安全 → 缓存友好

核心原则

签名时的 host == 验签时 OBS 实际收到的 Host 头。

任何破坏这条等式的操作(replace host、CDN 改回源 HOST、virtual addressing)都会导致SignatureDoesNotMatch

最彻底的解法:让 OBS 签名从链路中消失——鉴权交给 CDN 边缘,回源交给委托凭证,客户端只拼一个带auth_key的短 URL。

最终架构

你的服务器 │ 用 CDN_AUTH_KEY 算 MD5 │ 拼 auth_key 参数 ▼ 用户浏览器 │ GET https://oss.xxx.com/key?auth_key=6887a3fe-0-0-a1b2c3... ▼ CDN 边缘节点 │ 验 auth_key(时效 + MD5) │ 通过 → 有缓存直接吐 / 无缓存回源 ▼ OBS 私有桶 │ 验 CDN 委托凭证 │ 通过 → 返回对象 ▼ 用户拿到文件 ✅

没有X-Amz-*,没有 64 位 hex,没有 host 对齐,没有SignatureDoesNotMatch


写于 2026 年 7 月 28 日。从一个 403 错误出发,走了三天弯路,最终发现答案藏在"V2 不签 host"这五个字里。