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

NodeJS生成Google Cloud Media CDN有效签名URL故障排查

NodeJS生成Google Cloud Media CDN签名URL异常问题

使用NodeJS生成Google Cloud Media CDN签名URL时,出现部分过期时间戳有效、多数无效的异常:相邻的两个时间戳(如1735689598和1735689599)一个返回错误、一个正常可用。Media CDN配置无问题,Python版本的签名逻辑无论过期时间如何都能生成有效URL,但NodeJS无法直接复用Python生成的Ed25519私钥,两者密钥生成方式不同(Python使用Google文档提供的脚本,NodeJS使用命令行生成)。

可正常运行的Python代码

import base64
import datetime
import cryptography.hazmat.primitives.asymmetric.ed25519 as ed25519
from six.moves import urllib

def sign_url(
    url: str, key_name: str, base64_key: str, expiration_time: datetime.datetime
) -> str:
    stripped_url = url.strip()
    parsed_url = urllib.parse.urlsplit(stripped_url)
    query_params = urllib.parse.parse_qs(parsed_url.query, keep_blank_values=True)
    epoch = datetime.datetime.utcfromtimestamp(0)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    url_pattern = "{url}{separator}Expires={expires}&KeyName={key_name}"
    url_to_sign = url_pattern.format(
        url=stripped_url,
        separator="&" if query_params else "?",
        expires=expiration_timestamp,
        key_name=key_name,
    )

    digest = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key).sign(
        url_to_sign.encode("utf-8")
    )
    signature = base64.urlsafe_b64encode(digest).decode("utf-8")
    signed_url = "{url}&Signature={signature}".format(
        url=url_to_sign, signature=signature
    )

    return signed_url

# private key: tHfW65gTzws54calc2OzICdVvdu6X1BoonVLjsD5ZQU=
# public key: 5xujsWltI4LTi0Q92hp9HmA03KsJFn2WwziiOA3POm8=
url = 'https://media.radixsoft.dev/raindrops.mp4'
key_name = 'dev-keyset'
private_key = 'tHfW65gTzws54calc2OzICdVvdu6X1BoonVLjsD5ZQU='
expiration_timestamp = datetime.datetime.now() + datetime.timedelta(minutes = 10)

result = sign_url(url, key_name, private_key, expiration_timestamp)
print(result)

存在问题的NodeJS代码

const crypto = require('crypto');

// private key: MC4CAQAwBQYDK2VwBCIEIPzuffDsZ43sPxf1fuwiDrKsiSoyFw8aosF2jhS3VCez
// public key: JU4ouYJAZvD12RYH8-zhSmUTqT1ohN-1kcIKhr2jL4o=

function signURL(url, expiresTs, keyName, privateKey) {
  const toSign = `${url}?Expires=${expiresTs}&KeyName=${keyName}`;
  const signature = crypto.sign(null, Buffer.from(toSign), privateKey);

  return `${toSign}&Signature=${signature.toString('base64')}`;
}

const privateKey = crypto.createPrivateKey({
  key: Buffer.from('MC4CAQAwBQYDK2VwBCIEIPzuffDsZ43sPxf1fuwiDrKsiSoyFw8aosF2jhS3VCez', 'base64'),
  format: 'der',
  type: 'pkcs8',
});

const url = 'https://media.radixsoft.dev/raindrops.mp4';
const expiresTs = Math.round(new Date().getTime() / 1000) + 10 * 60; // 10分钟后过期(秒级时间戳)
// const expiresTs = 1735689598; // 2024-12-31 23:59:58 - 无效,返回错误
// const expiresTs = 1735689599; // 2024-12-31 23:59:59 - 有效
const keyName = 'dev-keyset';

const signedUrl = signURL(url, expiresTs, keyName, privateKey);
console.log(signedUrl);

错误返回信息

Google-Edge-Cache: Invalid signed request
Error: 114


问题诊断

  1. 签名编码格式不兼容:Python使用URL安全的Base64编码处理签名(替换+为-、/为_,去除末尾=),而NodeJS默认输出标准Base64编码,Google CDN仅识别URL安全格式的签名,这是导致多数时间戳签名无效的核心原因。
  2. 密钥格式不匹配:Python使用的是原始32字节Ed25519私钥,NodeJS加载的是PKCS#8格式的DER编码私钥,两者格式差异会导致签名逻辑底层解析不一致。
  3. 待签URL拼接逻辑缺陷:Python会根据原URL是否已有查询参数,动态选择&或?拼接参数,而NodeJS固定使用?,若原URL已有参数会导致格式错误(当前示例URL无参数,此问题暂未触发)。

修复步骤及完整代码

1. 统一签名的Base64编码格式

将NodeJS生成的签名转换为URL安全的Base64格式:

const urlSafeSignature = signature.toString('base64')
  .replace(/\+/g, '-')
  .replace(/\//g, '_')
  .replace(/=+$/, '');

2. 兼容密钥格式(可选:复用Python生成的私钥)

若需复用Python的原始Ed25519私钥,可直接加载原始格式密钥:

const rawPrivateKey = Buffer.from('tHfW65gTzws54calc2OzICdVvdu6X1BoonVLjsD5ZQU=', 'base64');
const privateKey = crypto.createPrivateKey({
  key: rawPrivateKey,
  format: 'raw',
  type: 'ed25519'
});

3. 修复待签URL拼接逻辑

对齐Python的参数拼接逻辑,避免原URL已有参数时的格式错误:

const separator = url.includes('?') ? '&' : '?';
const toSign = `${url}${separator}Expires=${expiresTs}&KeyName=${keyName}`;

完整修复后的NodeJS代码

const crypto = require('crypto');

// 可使用Python生成的原始私钥,或NodeJS命令行生成的PKCS#8密钥
const rawPrivateKey = Buffer.from('tHfW65gTzws54calc2OzICdVvdu6X1BoonVLjsD5ZQU=', 'base64');
const privateKey = crypto.createPrivateKey({
  key: rawPrivateKey,
  format: 'raw',
  type: 'ed25519'
});

function signURL(url, expiresTs, keyName, privateKey) {
  const separator = url.includes('?') ? '&' : '?';
  const toSign = `${url}${separator}Expires=${expiresTs}&KeyName=${keyName}`;
  const signature = crypto.sign(null, Buffer.from(toSign), privateKey);
  // 转换为URL安全的Base64
  const urlSafeSignature = signature.toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
  
  return `${toSign}&Signature=${urlSafeSignature}`;
}

const url = 'https://media.radixsoft.dev/raindrops.mp4';
const expiresTs = Math.round(Date.now() / 1000) + 10 * 60; // 10分钟后过期
const keyName = 'dev-keyset';

const signedUrl = signURL(url, expiresTs, keyName, privateKey);
console.log(signedUrl);

内容的提问来源于stack exchange,提问作者Hristo Hristov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 01:35:00