返回博客

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(有自定义元数据、同时传了 regionendpoint):必定 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——两者不一致,签名当然不匹配。

坑:x-oss-meta-* 键不能含下划线

调用方传来 { src_url: url, job_id: id },直接拼成 x-oss-meta-src_urlx-oss-meta-job_id,签名阶段和发送阶段看到的 header 名不同,必然触发 SignatureDoesNotMatch

陷阱二:同时传 regionendpoint

ali-oss SDK 内部有两套代码路径:一套根据 region 推导出标准 host(mybucket.oss-cn-xxx.aliyuncs.com),另一套使用显式 endpoint。当两个字段同时存在,SDK 会用 region 路径计算 canonical resource,却把请求实际打向 endpoint 指定的 host——签名是对一个 host 算的,请求发往另一个,OSS 服务端一验就炸。

坑:region 与 endpoint 不能同时传
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

正解:hyphenate 元数据键 + 二选一配置
  1. 所有 x-oss-meta-* 后缀键:下划线 → 连字符,全部 lowercase。
  2. regionendpoint 互斥:默认只传 region,内网场景只传 endpoint,绝不同时出现。

排查思路

两个陷阱都抛同一条错误,二分法剥离最有效:

  1. 先清空自定义元数据重试——若还报错,陷阱二(region/endpoint)是主因,先修它。
  2. 逐个加回元数据键,找到哪个键触发报错——该键含下划线,换成连字符即可。

也可以用矩阵测试快速定位陷阱一:

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 名里有没有下划线,以及 regionendpoint 是否共存