当前位置:首页 > 文章列表 > 文章 > 前端 > 跨语言HMAC签名差异及解决方法

跨语言HMAC签名差异及解决方法

2025-12-11 22:57:40 0浏览 收藏
推广推荐
免费电影APP ➜
支持 PC / 移动端,安全直达

编程并不是一个机械性的工作,而是需要有思考,有创新的工作,语法是固定的,但解决问题的思路则是依靠人的思维,这就需要我们坚持学习和更新自己的知识。今天golang学习网就整理分享《跨语言HMAC签名差异与解决方法》,文章讲解的知识点主要包括,如果你对文章方面的知识点感兴趣,就不要错过golang学习网,在这可以对大家的知识积累有所帮助,助力开发能力的提升。

解决跨语言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()所产生的字符串格式。

根据经验,许多系统在序列化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字符串的实践,开发者可以构建出更加健壮和可靠的跨语言安全验证机制。

今天带大家了解了的相关知识,希望对你有所帮助;关于文章的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~

Java安装后怎么配置Maven和GradleJava安装后怎么配置Maven和Gradle
上一篇
Java安装后怎么配置Maven和Gradle
SpringCloudConfig配置刷新机制解析
下一篇
SpringCloudConfig配置刷新机制解析
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    3292次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    3502次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    3533次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    4644次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    3911次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码