最简路径是直接用 openssl 的 hmac() 函数,但需严守参数顺序、正确传入密钥/消息长度(避免 strlen 截断)、用 &digest_len 获取实际长度,并注意返回指针指向静态缓冲区;若需流式计算或精细控制上下文,应选用 hmac_ctx(1.0.x 用 init/cleanup,1.1.1+ 用 new/free);base64 编码必须通过 bio_f_base64() 实现并禁用换行,不可用 hex 拼接;签名前的字符串拼接须严格对齐服务端规则,包括小写头名、去空格值、\n 分隔等,建议封装验证函数并比对原始字节数组。

直接用 HMAC() 函数是最简路径,但参数顺序和内存管理容易翻车
OpenSSL 的 HMAC() 是一个纯函数式接口,不用初始化上下文,适合一次性计算。但它要求你手动传入密钥长度、消息长度、摘要算法指针,且返回的 digest 内存由你负责释放(或用栈空间)。常见错误是把 key.length() 写成 strlen(key.c_str()) ——当 key 含 \0 字节时结果截断;或者忽略 digest_len 输出参数,直接按固定长度读取 32 字节,导致越界。
- 密钥和消息都必须用
const unsigned char*传入,std::string要显式转.c_str()并确保生命周期覆盖调用全程 -
HMAC()返回的digest指针指向内部静态缓冲区,不可 free,也不可长期持有(下次调用会覆盖) - SHA256 的输出长度恒为 32 字节,但务必用
&digest_len接收,别硬写32
HMAC_CTX 方式更可控,但初始化和清理不能省
老版本 OpenSSL(1.0.x)必须用 HMAC_CTX,新版本(1.1.1+)虽支持单次 HMAC(),但若需多次 update(比如流式计算),或想明确控制上下文生命周期,HMAC_CTX 仍是首选。问题在于:忘记 HMAC_CTX_init() 或漏掉 HMAC_CTX_cleanup() 会导致内存泄漏或后续调用崩溃;而 1.1.1+ 已弃用 HMAC_CTX_init(),改用 HMAC_CTX_new() + HMAC_CTX_free()。
- 1.0.x:
HMAC_CTX ctx; HMAC_CTX_init(&ctx); ... HMAC_CTX_cleanup(&ctx); - 1.1.1+:
HMAC_CTX* ctx = HMAC_CTX_new(); ... HMAC_CTX_free(ctx); - 不要混用:在 1.1.1+ 中调用
HMAC_CTX_init()会编译失败或运行时 SIGSEGV
Base64 编码必须用 BIO,手搓 hex 转字符串不等于 Base64
很多示例代码把 HMAC 结果转成 64 位十六进制字符串(如 "a1b2c3..."),这**不是** Base64。真实通信场景(如 HTTP Sign 头)要求的是 Base64 编码的二进制摘要。OpenSSL 的 BIO_f_base64() 是最稳妥的选择,但要注意关闭换行符(BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL)),否则生成的字符串带 \n,服务端校验必失败。
- 别用
sprintf("%02x")拼接——那是 hex 编码,长度是 64 字符,Base64 是约 44 字符 -
BIO_get_mem_ptr()返回的BUF_MEM*中data不以\0结尾,构造std::string必须传length - 如果用第三方 Base64 库(如 cppcodec),确认它处理的是原始 32 字节,而非 hex 字符串
签名拼接顺序必须和服务端完全一致,空格和换行都是破坏性差异
HMAC 本身不关心内容语义,只认字节流。所谓“按顺序合并所有消息”,意味着请求头字段名要小写、字段值要去首尾空格、多头同名要按出现顺序拼、\n 分隔符不能少也不能多。例如 GET\n/v1/api\na=1&b=2\ntoken:abc\nuser-id:123 和 GET\n/v1/api\na=1&b=2\ntoken: abc\nuser-id:123 算两个完全不同输入,HMAC 值必然不同。
立即学习“C++免费学习笔记(深入)”;
- 建议把拼接逻辑封装成独立函数,并用固定测试用例验证前后端输出一致
- 开发期用在线工具(如
lddgo.net/encrypt/hmac)比对中间结果,避免怀疑加密库有问题 - 服务端若用 Java 的
Mac.getInstance("HmacSHA256"),注意其默认使用 PKCS#5 填充——但 HMAC 本身无填充,实际只需确保密钥和消息字节数组完全一致
真正卡住人的往往不是算法调用,而是拼接规则没对齐、编码方式选错、或者密钥里有不可见字符。上线前拿一个已知明文+密钥,在两端各跑一次,逐字节比对 digest 原始字节数组,比看 Base64 更可靠。










