You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用签名URL向Google Storage上传文件时签名不匹配问题排查

解决Google Storage直传签名不匹配(SignatureDoesNotMatch)问题

你的上传请求出现SignatureDoesNotMatch错误,核心问题有三个:


1. 签名算法完全错误

Google Cloud Storage的签名URL要求使用RSA-SHA256算法签名,你代码里用了HMAC-SHA256,这完全不符合Google的签名规范,是导致签名不匹配的最主要原因。

2. 私钥处理方式错误

服务账号的私钥是PEM格式的RSA私钥,不能直接Base64解码后当作HMAC密钥导入,必须先清理PEM的头尾标记,再解码成二进制,最后导入为RSA私钥。

3. Content-Type硬编码

后端固定写死了image/png,如果前端上传其他类型文件,会导致签名里的Content-Type和实际请求不一致;即使当前传的是PNG,也应该保证前后端的Content-Type完全一致,动态传入才是合理做法。


修复后的代码

后端代码(修正签名逻辑)

export const getSignedUrl = async (
  bucketName: string,
  privateKey: string,
  accountEmail: string,
  contentType: string // 新增参数,接收前端传来的文件类型
) => {
  const expiration = Math.floor(Date.now() / 1000) + 60 * 60; // 1小时有效期
  const objectName = uuidv4();

  // 构造规范资源路径
  const canonicalResource = `/${bucketName}/${objectName}`;

  // 构造待签名字符串,注意换行和空行必须严格对应
  const stringToSign = [
    'PUT',
    '', // Content-MD5 留空
    contentType,
    expiration.toString(),
    canonicalResource
  ].join('\n');

  // 处理PEM私钥:去掉头尾标记,清理空格后Base64解码
  const cleanedPrivateKey = privateKey
    .replace(/-----BEGIN PRIVATE KEY-----/g, '')
    .replace(/-----END PRIVATE KEY-----/g, '')
    .replace(/\s+/g, '');
  const privateKeyBuffer = Uint8Array.from(atob(cleanedPrivateKey), c => c.charCodeAt(0));

  // 导入RSA私钥,指定RSASSA-PKCS1-v1_5算法和SHA-256哈希
  const key = await crypto.subtle.importKey(
    'pkcs8',
    privateKeyBuffer,
    { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
    false,
    ['sign']
  );

  // 生成RSA签名
  const signatureBuffer = await crypto.subtle.sign(
    'RSASSA-PKCS1-v1_5',
    key,
    new TextEncoder().encode(stringToSign)
  );

  // 签名转Base64并URL编码
  const signature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)));
  const encodedSignature = encodeURIComponent(signature);

  // 构造最终签名URL
  const signedUrl = `https://storage.googleapis.com/${bucketName}/${objectName}?GoogleAccessId=${accountEmail}&Expires=${expiration}&Signature=${encodedSignature}`;

  return { signedUrl, objectName }; // 返回文件名方便后续操作
};

前端代码(动态传入文件类型)

export const uploadFile = async (file: File) => {
  // 先调用后端获取对应文件类型的签名URL
  const { signedUrl } = await axios.post('/api/get-signed-url', {
    contentType: file.type
  });

  // 上传文件到签名URL
  const response = await axios.put(signedUrl, file, {
    headers: {
      'Content-Type': file.type,
    },
  });

  return response;
};

额外检查项

  • 确认服务账号拥有storage.objects.create权限,权限不足会导致签名正确但上传失败。
  • 对比Google错误返回中的StringToSign和你后端生成的是否完全一致(包括换行、空格、大小写),细微差异都会导致签名不匹配。
  • 检查Expires时间戳是秒级Unix时间戳,不要用毫秒级。

内容的提问来源于stack exchange,提问作者Felipe César

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 08:20:42