ali-oss SDK 签名不匹配:两个与 AccessKey 无关的隐藏陷阱
接到一个对象存储上传的任务:在某后台服务中封装 ali-oss 的 put() 方法,支持调用方传入任意自定义元数据。本地跑一个最小 node -e 脚本,凭同一套 AccessKey 上传毫无问题。但到了真实的 wrapper 里,每次上传都以同一个错误收场:
SignatureDoesNotMatchError: The request signature we calculated does not match
the signature you provided. Check your key and signing method.
错误信息语气笃定地指向"密钥有问题"——而密钥就是同一个。排查了两个小时,才发现是 ali-oss SDK 6.x 的两个独立 bug,各自都能单独触发这条错误,凑在一起更让人抓狂。
现象
- 最小复现脚本(无自定义元数据、单独传
region):上传成功。 - 生产 wrapper(有自定义元数据、同时传了
region和endpoint):必定SignatureDoesNotMatch。 - 把 wrapper 里的自定义元数据全去掉:依然报错。
- 再把
endpoint字段去掉:终于成功。
两个陷阱叠加,互相掩护。
根因
陷阱一:自定义元数据键里含下划线
ali-oss 6.x 在计算 canonical string 签名时,会对 header 名做 lowercase 处理;但发出 HTTP 请求时,Node.js 底层的某些路径会把 header 名中的下划线转成连字符(hyphen)。于是签名时算的是 x-oss-meta-src_url,发出去的实际是 x-oss-meta-src-url——两者不一致,签名当然不匹配。
调用方传来 { src_url: url, job_id: id },直接拼成 x-oss-meta-src_url 和 x-oss-meta-job_id,签名阶段和发送阶段看到的 header 名不同,必然触发 SignatureDoesNotMatch。
陷阱二:同时传 region 和 endpoint
ali-oss SDK 内部有两套代码路径:一套根据 region 推导出标准 host(mybucket.oss-cn-xxx.aliyuncs.com),另一套使用显式 endpoint。当两个字段同时存在,SDK 会用 region 路径计算 canonical resource,却把请求实际打向 endpoint 指定的 host——签名是对一个 host 算的,请求发往另一个,OSS 服务端一验就炸。
new OSS({
region: 'oss-cn-beijing',
endpoint: 'oss-cn-beijing.aliyuncs.com', // ← 冗余且危险
bucket: 'mybucket',
accessKeyId: '...',
accessKeySecret: '...',
})两个字段并存时,SDK 的签名计算与请求目标产生分裂,报错同样是 SignatureDoesNotMatch,误导你继续查密钥。
正解
修复陷阱一:元数据键统一 hyphenate
在 wrapper 入口做一次规范化,把所有下划线替换成连字符:
function buildOssMeta(metadata: Record<string, string>) {
const headers: Record<string, string> = {}
for (const [k, v] of Object.entries(metadata)) {
const safeKey = k.replace(/_/g, '-').toLowerCase()
headers[`x-oss-meta-${safeKey}`] = encodeURIComponent(v)
}
return headers
}调用方无感,键规范化在内部透明处理。
修复陷阱二:只传 region,按需才加 endpoint
const config: OSSConfig = {
region: process.env.OSS_REGION,
accessKeyId: process.env.OSS_KEY_ID,
accessKeySecret: process.env.OSS_KEY_SECRET,
bucket: process.env.OSS_BUCKET,
secure: true,
}
// 仅在 ECS 内网环境下才追加内网 endpoint
if (process.env.OSS_INTERNAL_ENDPOINT) {
config.endpoint = process.env.OSS_INTERNAL_ENDPOINT
}
const oss = new OSS(config)默认路径永远只传 region,让 SDK 自己推导公网 host;内网加速是唯一需要 endpoint 的场景,且此时不传 region。
- 所有
x-oss-meta-*后缀键:下划线 → 连字符,全部 lowercase。 region与endpoint互斥:默认只传region,内网场景只传endpoint,绝不同时出现。
排查思路
两个陷阱都抛同一条错误,二分法剥离最有效:
- 先清空自定义元数据重试——若还报错,陷阱二(region/endpoint)是主因,先修它。
- 逐个加回元数据键,找到哪个键触发报错——该键含下划线,换成连字符即可。
也可以用矩阵测试快速定位陷阱一:
for (const headers of [
{ 'x-oss-meta-srcurl': 'test' }, // 无分隔符 → OK
{ 'x-oss-meta-src_url': 'test' }, // 下划线 → FAIL
{ 'x-oss-meta-src-url': 'test' }, // 连字符 → OK
]) {
await oss.put('probe/' + Date.now(), Buffer.from('x'), { headers })
}一句话外卖
ali-oss 的
SignatureDoesNotMatch几乎从不是密钥问题——优先排查 header 名里有没有下划线,以及region和endpoint是否共存。