首页 > web前端 > js教程 > 正文

解决跨语言HMAC签名验证不一致:JSON字符串化差异与标准化实践

DDD
发布: 2025-11-15 19:48:01
原创
707人浏览过

解决跨语言HMAC签名验证不一致:JSON字符串化差异与标准化实践

本文深入探讨了在跨语言(如python与typescript)进行hmac签名验证时,因json字符串化方式差异导致验证失败的常见问题。文章详细分析了问题根源,并提供了一套基于typescript的健壮解决方案,通过标准化json对象的字符串表示,确保了签名数据在不同语言环境下的完全一致性,从而实现可靠的hmac验证。

引言

HMAC(Keyed-Hash Message Authentication Code)签名验证是确保API请求或Webhook数据完整性和真实性的重要安全机制。它通过共享密钥对数据进行哈希计算,生成一个签名,接收方使用相同的密钥和数据再次计算签名,并与发送方提供的签名进行比对。然而,在实际开发中,当发送方和接收方使用不同编程语言实现HMAC验证时,一个常见的陷阱是由于数据序列化(特别是JSON字符串化)方式的细微差异,导致签名验证失败。本文将以Python和TypeScript为例,详细剖析这一问题并提供一套通用的解决方案。

HMAC签名验证基础

HMAC签名的基本流程如下:

  1. 共享密钥(Secret Key):发送方和接收方预先共享一个秘密密钥。
  2. 待签名数据(Data to Sign):将要验证的数据(通常包括时间戳和请求体等)拼接成一个字符串。
  3. 哈希算法(Hashing Algorithm):选择一个加密哈希算法(如SHA256)。
  4. 计算签名:使用共享密钥、待签名数据和哈希算法计算HMAC值。
  5. 比较签名:接收方使用相同的方法计算签名,并与发送方提供的签名进行安全比较。

以下是Python中一个典型的HMAC签名验证函数示例:

import hmac
import hashlib
import json

def verify_signature(key, timestamp, provided_signature, payload):
  key_bytes = bytes.fromhex(key)
  payload_str = json.dumps(payload) # JSON字符串化
  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
登录后复制

跨语言验证的陷阱:JSON字符串化差异

问题通常出现在“待签名数据”的构建环节,特别是当数据中包含JSON对象时。Python的json.dumps()和TypeScript(或JavaScript)的JSON.stringify()函数在将JSON对象转换为字符串时,其默认行为可能存在差异,例如:

  • 空格和换行符: 默认情况下,JSON.stringify()会生成紧凑的字符串,不包含多余的空格或换行符。而json.dumps()在某些情况下或不指定separators参数时,可能会在键值对之间、数组元素之间或对象属性之间包含空格。
  • 键的顺序: 虽然JSON规范不保证对象的键顺序,但在实践中,不同的JSON库或版本在序列化时可能会以不同的顺序输出键,这会导致最终字符串不同。

即使是单个空格或换行符的差异,也会导致待签名数据字符串完全不同,进而产生不同的HMAC签名,最终导致验证失败。

例如,一个初始的TypeScript尝试可能如下:

import * as crypto from 'crypto';

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 || !providedSignature) {
    throw new Error('[verifyCloseSignature] Required headers 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'),
  );
}
登录后复制

这段TypeScript代码与Python的verify_signature函数在处理payload的字符串化时存在差异,可能导致计算出的签名与Python端不一致。

标准化JSON字符串以确保一致性

解决此问题的关键在于确保发送方和接收方在将JSON对象转换为字符串时,其结果字符串是完全一致的。这意味着TypeScript需要精确地模拟Python端json.dumps()所产生的字符串格式。

Find JSON Path Online
Find JSON Path Online

Easily find JSON paths within JSON objects using our intuitive Json Path Finder

Find JSON Path Online 30
查看详情 Find JSON Path Online

根据经验,许多系统在序列化JSON时会采用一种“相对紧凑但保留特定空格”的格式,例如在冒号后保留一个空格,在逗号后保留一个空格,但移除其他不必要的空格和换行。

我们可以创建一个辅助函数来标准化JSON对象的字符串表示。以下是一个使用Ramda库进行函数式编程和正则表达式替换的TypeScript实现:

import { pipe, replace } from 'ramda'; // 假设已安装ramda

/**
 * 将JSON对象标准化为特定格式的字符串。
 * 该函数旨在模拟Python json.dumps()在特定场景下产生的字符串格式,
 * 确保在冒号和逗号后有一个空格,移除其他不必要的空格和换行。
 * @param object 待字符串化的JSON对象。
 * @returns 标准化后的JSON字符串。
 */
export const toJSONWithSpaces = pipe(
  // 1. 使用 null, 1 参数进行初步字符串化,产生带换行和缩进的字符串
  (object: unknown) => JSON.stringify(object, null, 1),
  // 2. 将换行符和其后的空格替换为单个空格
  replace(/\n +/gm, ' '),
  // 3. 确保冒号后有一个空格
  replace(/:\s?/g, ': '), // 匹配冒号后可能有或没有的空格,替换为冒号后一个空格
  // 4. 移除开括号后的空格
  replace(/{\s?/g, '{'),
  // 5. 移除闭括号前的空格
  replace(/\s?}/g, '}'),
  // 6. 移除开方括号后的空格
  replace(/\[\s?/g, '['),
  // 7. 移除闭方括号前的空格
  replace(/\s?]/g, ']'),
  // 8. 确保逗号后有一个空格
  replace(/,\s?/g, ', '),
);
登录后复制

这个toJSONWithSpaces函数通过一系列的正则表达式替换,将JSON.stringify(object, null, 1)(带缩进和换行)的输出转换为一个精确控制空格的字符串。它确保了在冒号和逗号后有一个空格,同时移除了其他地方的空格,以匹配Python端可能产生的特定格式。

完整的TypeScript签名验证实现

将上述toJSONWithSpaces函数集成到签名验证逻辑中,可以得到一个健壮的TypeScript验证函数:

import * as crypto from 'crypto';
import { toJSONWithSpaces } from './utils'; // 假设toJSONWithSpaces在utils.ts中

/**
 * 验证Close API或Webhook签名。
 * @param request Express或其他框架的请求对象,包含headers。
 * @param key 用于HMAC计算的密钥(十六进制字符串)。
 * @param payload 请求体中的JSON数据。
 * @returns 签名是否有效。
 * @throws 如果缺少必要的签名头部信息。
 */
export function verifyCloseSignature(
  request: Request, // 或 Express.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');
  }

  // 使用标准化函数处理payload,确保字符串格式与Python端一致
  const cleanedPayload = toJSONWithSpaces(payload);

  const hmac = crypto.createHmac('sha256', Buffer.from(key, 'hex'));
  hmac.update(timestamp + cleanedPayload); // 使用标准化的payload
  const calculatedSignature = hmac.digest('hex');

  // 使用crypto.timingSafeEqual进行安全比较,防止时序攻击
  return crypto.timingSafeEqual(
    Buffer.from(providedSignature, 'hex'),
    Buffer.from(calculatedSignature, 'hex'),
  );
}
登录后复制

通过使用toJSONWithSpaces处理payload,我们确保了timestamp + cleanedPayload这个待签名数据字符串在Python和TypeScript两端是完全相同的,从而解决了HMAC签名验证不一致的问题。

注意事项

  1. JSON键顺序: 尽管上述解决方案主要关注空格,但某些JSON库在序列化时可能会以不同的顺序输出对象键。如果源系统(例如Python)的json.dumps在不指定sort_keys=True的情况下产生了特定但非标准顺序的键,那么目标系统(TypeScript)也需要能够以相同顺序序列化。在大多数情况下,如果源系统是确定性的,并且目标系统也通过某种方式(如先对键进行排序再字符串化)来保证确定性,则可以避免此问题。本教程中的toJSONWithSpaces不直接处理键排序,但其目的是匹配一个已知的Python输出格式。
  2. 安全性: 在比较签名时,务必使用crypto.timingSafeEqual(Node.js)或类似的安全比较函数。直接使用===或==进行字符串比较可能会存在时序攻击的风险。
  3. 依赖管理: 上述toJSONWithSpaces函数使用了Ramda库。在实际项目中,你需要安装ramda (npm install ramda 或 yarn add ramda)。如果不想引入外部库,也可以使用纯JavaScript/TypeScript实现类似的字符串替换逻辑。
  4. 错误处理: 确保对缺失的必要头部信息(如时间戳和签名)进行适当的错误检查和处理。
  5. 时间戳: 时间戳通常也作为待签名数据的一部分,其格式和精度也需要保持一致。

总结

跨语言HMAC签名验证中的不一致问题,往往源于数据序列化过程中的细微差异,尤其是JSON字符串化时对空格、换行符和键顺序的处理。解决之道在于精确地标准化待签名数据的字符串表示,确保发送方和接收方在计算HMAC时使用完全相同的原始数据。通过本教程提供的TypeScript标准化JSON字符串的实践,开发者可以构建出更加健壮和可靠的跨语言安全验证机制。

以上就是解决跨语言HMAC签名验证不一致:JSON字符串化差异与标准化实践的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号