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
问题诊断
- 签名编码格式不兼容:Python使用URL安全的Base64编码处理签名(替换
+为-、/为_,去除末尾=),而NodeJS默认输出标准Base64编码,Google CDN仅识别URL安全格式的签名,这是导致多数时间戳签名无效的核心原因。 - 密钥格式不匹配:Python使用的是原始32字节Ed25519私钥,NodeJS加载的是PKCS#8格式的DER编码私钥,两者格式差异会导致签名逻辑底层解析不一致。
- 待签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
相关产品推荐
相关产品推荐

