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

本文深入探讨了在跨语言(如python与typescript)进行hmac签名验证时,因json字符串化方式差异导
致验证失败的常见问题。文章详细分析了问题根源,并提供了一套基于typescript的健壮解决方案,通过标准化json对象的字符串表示,确保了签名数据在不同语言环境下的完全一致性,从而实现可靠的hmac验证。
引言
HMAC(Keyed-Hash Message Authentication Code)签名验证是确保API请求或Webhook数据完整性和真实性的重要安全机制。它通过共享密钥对数据进行哈希计算,生成一个签名,接收方使用相同的密钥和数据再次计算签名,并与发送方提供的签名进行比对。然而,在实际开发中,当发送方和接收方使用不同编程语言实现HMAC验证时,一个常见的陷阱是由于数据序列化(特别是JSON字符串化)方式的细微差异,导致签名验证失败。本文将以Python和TypeScript为例,详细剖析这一问题并提供一套通用的解决方案。
HMAC签名验证基础
HMAC签名的基本流程如下:
- 共享密钥(Secret Key):发送方和接收方预先共享一个秘密密钥。
- 待签名数据(Data to Sign):将要验证的数据(通常包括时间戳和请求体等)拼接成一个字符串。
- 哈希算法(Hashing Algorithm):选择一个加密哈希算法(如SHA256)。
- 计算签名:使用共享密钥、待签名数据和哈希算法计算HMAC值。
- 比较签名:接收方使用相同的方法计算签名,并与发送方提供的签名进行安全比较。
以下是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(或J*aScript)的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()所产生的字符串格式。
Visla
AI视频生成器,快速轻松地将您的想法转化为视觉上令人惊叹的视频。
100
查看详情
根据经验,许多系统在序列化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签名验证不一致的问题。
注意事项
- JSON键顺序: 尽管上述解决方案主要关注空格,但某些JSON库在序列化时可能会以不同的顺序输出对象键。如果源系统(例如Python)的json.dumps在不指定sort_keys=True的情况下产生了特定但非标准顺序的键,那么目标系统(TypeScript)也需要能够以相同顺序序列化。在大多数情况下,如果源系统是确定性的,并且目标系统也通过某种方式(如先对键进行排序再字符串化)来保证确定性,则可以避免此问题。本教程中的toJSONWithSpaces不直接处理键排序,但其目的是匹配一个已知的Python输出格式。
- 安全性: 在比较签名时,务必使用crypto.timingSafeEqual(Node.js)或类似的安全比较函数。直接使用===或==进行字符串比较可能会存在时序攻击的风险。
- 依赖管理: 上述toJSONWithSpaces函数使用了Ramda库。在实际项目中,你需要安装ramda (npm install ramda 或 yarn add ramda)。如果不想引入外部库,也可以使用纯J*aScript/TypeScript实现类似的字符串替换逻辑。
- 错误处理: 确保对缺失的必要头部信息(如时间戳和签名)进行适当的错误检查和处理。
- 时间戳: 时间戳通常也作为待签名数据的一部分,其格式和精度也需要保持一致。
总结
跨语言HMAC签名验证中的不一致问题,往往源于数据序列化过程中的细微差异,尤其是JSON字符串化时对空格、换行符和键顺序的处理。解决之道在于精确地标准化待签名数据的字符串表示,确保发送方和接收方在计算HMAC时使用完全相同的原始数据。通过本教程提供的TypeScript标准化JSON字符串的实践,开发者可以构建出更加健壮和可靠的跨语言安全验证机制。
以上就是解决跨语言HMAC签名验证不一致:JSON字符串化差异与标准化实践的详细内容,更多请关注其它相关文章!
# 换行
# 秋冬童装网站推广策划
# 通化优化网站有哪些
# 江油怎么做网站推广
# 静安区营销推广展示中心
# 数字营销一键推广测款
# 浙江seo排名前十企业
# 营销渠道包括推广渠道吗
# 西藏小语种网站推广
# seo自学网下载
# 富县门户网站建设
# 会以
# 并与
# 情况下
# 转换为
# javascript
# 换行符
# 有一个
# 序列化
# 移除
# typescr
# 正则表达式
# go
# node
# json
# node.js
# js
# java
# python
相关栏目:
【
科技资讯46185 】
【
网络学院92790 】
相关推荐:
纯CSS与HTML网格布局的HTML精简策略:SVG与JS方案解析
C++如何实现异步操作_C++11使用std::future和std::async进行异步编程
汽水音乐车机版8.9下载 汽水音乐车机版8.9版本安装入口
解决Rails应用中内容错位与Turbo警告:meta标签误用导致富文本渲染异常
Linux如何排查内存不足OOME问题_LinuxOOM分析教程
处理Kafka消费者会话超时:深入理解消息处理语义与幂等性
cad如何更改注释性对象的比例_cad注释性比例调整方法
Win11怎么设置鼠标指针速度_Win11提高鼠标指针精确度选项
QQ官网正版登录链接 QQ在线登录入口最新
Pandas DataFrame 高效批量赋值:告别循环与笛卡尔积误区
Golang如何优雅处理error_Golang error处理最佳实践总结
uc浏览器网页版极速入口 uc网页浏览器网页版流畅体验
怎样把文件彻底粉碎无法恢复_Windows下安全删除敏感数据【隐私保护】
谷歌学术网站直达地址 谷歌学术搜索网页版一键进入
抖音从哪里进入网页版_抖音官方入口链接
CSS Box Model与弹性按钮:维持布局稳定的动画实践
小米汽车11月交付量突破40000台!雷军:将继续努力
QQ邮箱登录官网首页 腾讯QQ邮箱网页入口
PDF怎么合并PDF并保持格式_PDF合并文件保持排版教程
字由网在线版登录地址 字由网网页版安全入口
J*a 递归快速排序中静态变量的状态管理与陷阱
vivo浏览器怎么扫描二维码 vivo浏览器内置扫一扫功能使用方法
CSS子选择器:如何区分并样式化嵌套列表的子层级
向日葵客户端怎么进行远程CentOS控制_向日葵客户端远程CentOS控制操作教程
QQ邮箱电脑版登录入口_QQ邮箱官方网站登录平台
在Go开发中优雅管理ListenAndServe进程:GoSublime集成方案
Descript怎样用AI剪辑自动去噪_Descript用AI剪辑自动去噪【自动降噪】
处理动态列数据:J*a ArrayList的正确初始化与字符累加教程
漫蛙漫画登录站点 漫蛙2正版漫画快速访问
海棠账号登录入口_登录海棠账户同步阅读记录
2026春节假期票务安排_2026春节放假购票指南
Golang如何使用const iota_Go iota常量计数器讲解
Typer应用中灵活处理命令行参数的令牌化与解析
mysql备份恢复性能优化_mysql备份恢复性能优化方法
微信商城在哪里打开【步骤】
MAC怎么让Dock栏只显示当前运行的应用_MAC终端命令实现极简Dock栏
蛙漫漫画免费阅读入口_蛙漫官方正版无广告纯净版
163邮箱登录密码 163邮箱忘记密码找回
PHP表单数据传递:如何通过隐藏输入字段获取动态ID
XML中包含HTML标签导致解析错误? 正确嵌入非XML数据的两种方法
如何设置Windows Defender的定时扫描_计划任务实现自动杀毒【安全】
顺丰快件物流信息 官方网站查询入口
抖音商城签到领现金是真的吗_抖音商城签到奖励与提现说明
快手赚钱渠道_快手收益来源
Lar*el 递归关系中排除指定分支的教程
解决Python logging 中 datefmt 导致时间戳固定不变的问题
Golang切片为何属于引用类型_Golang slice底层结构与引用语义说明
包子漫画官方网站阅读入口-包子漫画在线漫画官网直达链接
夸克浏览器网页版最新地址 夸克浏览器官方入口合集
解决Django多数据库/多Schema环境下外键迁移问题


2025-11-15
浏览次数:次
返回列表