很多人第一次调用阿里云 API 时,卡住的常常是返回的“签名错误”。密钥明明没问题,请求却被拒绝,最后查出来可能只是服务器时间慢了几秒,或者查询字符串的顺序没排对。这篇文章先说签名要解决什么问题,再用查询 ECS 实例的例子走一遍流程,最后讲报错时该先看哪几处。

签名到底在防什么
OpenAPI 开放在公网上,拿到请求的人可以尝试改参数,也可以把请求原样重放一遍。签名主要有三个作用。一是确认请求来自持有 AccessKey 的人。二是保证参数在传输途中没被改动,改了签名就对不上。三是配合时间戳和随机数(Nonce),让同一个请求没法被反复使用。
签名不是加密。请求内容仍然是明文,生产环境还是要走 HTTPS。
V1 和 V3 怎么选
阿里云目前主要有两套签名机制。V1 基于 HMAC-SHA1,拼接规则比较多,手写很容易出错。V3 使用 ACS3-HMAC-SHA256,结构更清楚,官方 SDK 默认也是这一套。新项目直接用 V3。老项目如果已经稳定运行,不必急着改,排进迁移计划就行。
V3 签名的四步
V3 签名分四步,前一步的输出是后一步的输入。
- 构造规范请求(CanonicalRequest):把 HTTP 方法、规范化的 URI、排序后的查询字符串、规范化的请求头、参与签名的头列表和请求体哈希,用换行符拼起来。
- 构造待签字符串(StringToSign):算法名 ACS3-HMAC-SHA256 加上上一步结果的 SHA256 哈希值。
- 计算签名:用 AccessKeySecret 作为密钥,对待签字符串做 HMAC-SHA256,结果转成小写十六进制。
- 放进请求头:Authorization 里带上 Credential、SignedHeaders 和 Signature。
第一步最容易出问题。查询参数要先按 RFC3986 编码,空格编码成 %20,不能用加号,然后按参数名字典序排列。请求头名称统一小写,只签参与签名的那几个,比如 host、x-acs-action、x-acs-date。
实战:用 Python 调用 DescribeInstances
下面以 ECS 的 DescribeInstances 接口为例。代码只用了标准库,适合用来理解原理。真正上线时,还是建议直接用阿里云官方 SDK。
import hashlib
import hmac
import os
from urllib.parse import quote
ACCESS_KEY_ID = os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"]
ACCESS_KEY_SECRET = os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"]
def sha256_hex(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
def rfc3986_encode(value: str) -> str:
# RFC3986 编码,空格会变成 %20,而不是 +
return quote(str(value), safe="-_.~")
def sign(method, canonical_uri, query, headers, signed_headers, body):
# 1. 查询字符串:按参数名排序,并做 RFC3986 编码
canonical_query = "&".join(
f"{rfc3986_encode(k)}={rfc3986_encode(v)}"
for k, v in sorted(query.items())
)
# 2. 规范化请求头:名称必须小写,按字典序排列
canonical_headers = "".join(
f"{k}:{headers[k].strip()}\n" for k in sorted(signed_headers)
)
hashed_payload = sha256_hex(body)
canonical_request = "\n".join([
method,
canonical_uri,
canonical_query,
canonical_headers,
";".join(sorted(signed_headers)),
hashed_payload,
])
# 3. 构造待签字符串
string_to_sign = "ACS3-HMAC-SHA256\n" + sha256_hex(canonical_request)
# 4. 计算签名
signature = hmac.new(
ACCESS_KEY_SECRET.encode("utf-8"),
string_to_sign.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return signature
实际调用时有两个字段要特别留意。x-acs-date 必须是 UTC 时间,格式为 yyyy-MM-ddTHH:mm:ssZ。x-acs-signature-nonce 每次请求都要生成新的 UUID。这两个字段是防重放的关键,也是最常见的出错点。
报错时先看哪几处
签名失败大多集中在下面几类。
- SignatureDoesNotMatch 多半是规范请求拼错了,常见问题在查询字符串的排序和编码。用官方 SDK 发出同样的请求,把两边的 CanonicalRequest 打印出来逐行对比。
- Forbidden.RAM 或 NoPermission 通常说明签名是对的,只是这个 RAM 用户没有调用该接口的权限。去 RAM 控制台补充策略即可,不必再改签名代码。
- 时间相关错误多见于容器和虚拟机,原因是服务器时间和 UTC 偏差过大。开启 NTP 时间同步,并确认 x-acs-date 用的是 UTC。
- InvalidAccessKeyId.NotFound 一般是 AK 填错了,或者用了另一个账号的密钥。核对 AK ID 和它所属的账号。
排查时,把每个阶段的字符串打印出来,和官方文档或 SDK 的输出逐段比对。凭感觉改代码,往往会越改越乱。
别和短信签名混在一起
搜“阿里云 签名”时,经常会看到短信签名。它是发短信时显示在内容开头的【企业名称】,需要在控制台提交申请,审核通过后才能使用。两者名字相同,用途却完全不同。接口鉴权的签名用来校验请求是否合法,短信签名只决定短信内容前面显示什么。如果你要发验证码,先申请短信签名,再配合短信模板完成开通。
密钥安全
签名只有建立在密钥安全的基础上才有意义。几条建议如下。
- 不要把 AccessKey 写死在代码或公开仓库里,放到环境变量或密钥管理服务中。
- 业务使用 RAM 子账号,按最小权限授权,不要直接拿主账号的密钥跑业务。
- 如果运行在阿里云 ECS 或容器服务上,优先使用角色和 STS 临时凭证,减少长期密钥留在机器上的时间。
- 定期轮换 AccessKey。一旦怀疑泄露,先禁用旧密钥,再生成新的。
报错时,按四步逐段对照,就能定位到具体是哪一步出了问题。如果时间不够,先用官方 SDK 跑通一次调用,再决定要不要深入手写签名逻辑。