Python转TypeScript的Close Webhook签名验证代码为何不等效?
Close Webhook签名验证:Python转TypeScript后验证失败问题
我需要验证Close Webhook的签名,官方仅提供Python版本的示例代码,我将其转换为TypeScript后,相同数据下Python验证通过,但TypeScript验证失败,求问两段代码为何不等效?
官方Python签名验证代码
import hmac import hashlib import json def verify_signature(key, timestamp, provided_signature, payload): key_bytes = bytes.fromhex(key) payload_str = json.dumps(payload) data = timestamp + payload_str signature = hmac.new(key_bytes, data.encode('utf-8'), hashlib.sha256).hexdigest() valid = hmac.compare_digest(provided_signature, signature) return valid
转换后的TypeScript代码
export function verifyCloseSignature( request: Request, key: string, payload: any, ) { const headers = request.headers; const timestamp = headers.get('close-sig-timestamp'); const providedSignature = headers.get('close-sig-hash'); if (!timestamp) { throw new Error('[verifyCloseSignature] Required timestamp header missing'); } if (!providedSignature) { throw new Error('[verifyCloseSignature] Required signature header missing'); } const payloadString = JSON.stringify(payload); const hmac = crypto.createHmac('sha256', Buffer.from(key, 'hex')); hmac.update(timestamp + payloadString); const calculatedSignature = hmac.digest('hex'); return crypto.timingSafeEqual( Buffer.from(providedSignature, 'hex'), Buffer.from(calculatedSignature, 'hex'), ); }
测试逻辑
const headers = new Headers(); headers.set('close-sig-hash', signature); headers.set('close-sig-timestamp', timestamp.toString()); headers.set('Content-Type', 'application/json'); const request = new Request(faker.internet.url(), { method: 'POST', headers: headers, body: JSON.stringify(payload), }); const actual = verifyCloseSignature(request, key, payload); const expected = true; expect(actual).toEqual(expected);
核心差异:JSON序列化格式不一致
两段代码最关键的区别在于JSON序列化的输出格式:
- Python的
json.dumps()默认使用(', ', ': ')作为分隔符,生成的JSON字符串会在冒号后、逗号后添加空格,例如{"key": "value", "list": [1, 2, 3]}。 - TypeScript的
JSON.stringify()默认使用(',', ':')作为分隔符,生成的JSON字符串没有额外空格,例如{"key":"value","list":[1,2,3]}。
这种格式差异会导致timestamp + payload_str的最终字符串完全不同,进而使得HMAC-SHA256计算出的签名不一致,最终验证失败。
解决方法
要让TypeScript代码和Python代码等效,需要让TypeScript生成与Pythonjson.dumps()默认格式完全一致的JSON字符串。有两种可行方案:
方案1:修改TypeScript的JSON序列化逻辑
由于JSON.stringify()没有直接设置分隔符的参数,你可以对其结果进行后处理,添加必要的空格(适用于基础JSON结构,复杂嵌套需更严谨的处理):
// 生成匹配Python默认格式的JSON字符串 function pythonStyleJsonStringify(obj: any): string { return JSON.stringify(obj) .replace(/:/g, ': ') .replace(/,/g, ', '); } // 在验证函数中替换原有的JSON.stringify调用 const payloadString = pythonStyleJsonStringify(payload);
方案2:修改Python代码的序列化逻辑(如果允许)
如果可以调整Python测试代码的序列化方式,让它生成与TypeScript默认格式一致的无空格JSON字符串:
# 修改json.dumps使用无空格分隔符 payload_str = json.dumps(payload, separators=(',', ':'))
生产环境优化建议
如果是验证真实Webhook请求,建议直接使用请求原始的body内容,而非重新序列化传入的payload对象——避免因对象解析/序列化过程中丢失格式信息导致的签名不匹配。例如在TypeScript中直接读取请求的原始body:
export async function verifyCloseSignature( request: Request, key: string, ) { const headers = request.headers; const timestamp = headers.get('close-sig-timestamp'); const providedSignature = headers.get('close-sig-hash'); if (!timestamp) { throw new Error('[verifyCloseSignature] Required timestamp header missing'); } if (!providedSignature) { throw new Error('[verifyCloseSignature] Required signature header missing'); } // 读取原始请求body const payloadString = await request.text(); const hmac = crypto.createHmac('sha256', Buffer.from(key, 'hex')); hmac.update(timestamp + payloadString); const calculatedSignature = hmac.digest('hex'); return crypto.timingSafeEqual( Buffer.from(providedSignature, 'hex'), Buffer.from(calculatedSignature, 'hex'), ); }
内容的提问来源于stack exchange,提问作者J. Hesters
相关产品推荐
相关产品推荐

