华为云 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-PAYLOADX-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) | |
|---|---|---|---|
| 发布年份 | ~2006 | 2011 | 2022 |
| 哈希算法 | HMAC‑SHA1 | HMAC‑SHA256 | ECDSA P‑256 + SHA256 |
| 签名输出 | 20B → Base64 →28 字符 | 32B → Hex →64 字符 | DER → Base64 →~72 字符 |
| 签 Host? | ❌不签 | ✅ 签 | ✅ 签 |
| 签 Region? | ❌ | ✅ | ✅(可写*) |
| 密钥派生 | 直接用 SK | 4 层 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%3D4.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=9b4483004584032cc1c9091cb987affd3f84e5d5b4b86f9bc42bf3858950775f4.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) (桶仍私有)| 层 | 谁验 | 验什么 | 作用 |
|---|---|---|---|
| ① 用户→CDN | CDN 边缘节点 | auth_key(时间戳+MD5) | 防盗链、防遍历、限时 |
| ② CDN→OBS | OBS | CDN 用委托凭证签名 | 桶保持私有 |
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}| 字段 | 含义 |
|---|---|
timestamp | Unix 时间戳(十六进制) |
rand | 随机串(可为0) |
uid | 用户标识(可为0) |
md5hash | MD5("{uri}-{timestamp}-{rand}-{uid}-{PrivateKey}") |
CDN 边缘节点收到请求后:
- 没有
auth_key→ 403 timestamp + 有效时长 < 当前时间→ 过期 403- 用 PrivateKey 重算 MD5,对不上 → 403
- 全通过 → 放行(有缓存直接吐,没缓存才回源)
关键优势:验签在 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九、控制台配置清单
| # | 位置 | 操作 | 说明 |
|---|---|---|---|
| 1 | CDN → 域名 → 源站配置 | 源站类型 = OBS 桶;开启OBS 回源鉴权+ 委托授权 | 让 CDN 能读私有桶 |
| 2 | CDN → 域名 → 访问控制 →URL 鉴权 | 方式 A;PrivateKey = 你的密钥;有效时长 = 3600s | 与代码里CDN_AUTH_KEY一致 |
| 3 | CDN → 域名 → 缓存配置 | 忽略auth_key参数(或忽略全部 URL 参数) | ⚠️ 极易遗漏!否则缓存命中率归零 |
| 4 | OBS 桶 → 权限 | 保持私有 | 不要开公共读 |
| 5 | IAM → 委托 | 创建 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_style用virtual
华为云 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"这五个字里。