HMACSHA256签名失败主因是参数拼接、编码或密钥处理不一致;须按ASCII字典序排序所有参数(含timestamp、nonce及空值),URL编码用Uri.EscapeDataString,密钥和原文均需UTF-8字节数组,输出统一用Base64或小写十六进制,验签前必须校验时间戳和nonce,并用FixedTimeEquals防时序攻击。

直接用 HMACSHA256 类就能生成签名,但 90% 的失败不是算法问题,而是参数拼接、编码或密钥处理不一致导致的——服务端和客户端算出的签名对不上,本质是“同文不同字”。
拼接参数必须严格按字典序 + URL 编码
只对业务字段签名是无效的。timestamp、nonce、所有请求参数(包括空值)都得参与拼接,否则防篡改和防重放都失效。
- 参数先按 key 名 ASCII 升序排序(
ksort或SortedDictionary<string, string>),不能依赖原始 JSON 或表单顺序 - 每个 value 必须用
System.Web.HttpUtility.UrlEncode或Uri.EscapeDataString编码(注意:不是Uri.EscapeUriString) - 拼接格式统一为
key1=value1&key2=value2&...,前后不加空格,末尾不加 & - 如果对接 PHP/Java 端,确认对方用的是
rawurlencode——它把空格转成%20,而UrlEncode在 .NET 中默认也符合这一行为
密钥和原文必须用 UTF-8 字节数组传入
HMACSHA256 构造函数只接受 byte[],传字符串会静默失败或抛 CryptographicException。
- 密钥处理:
Encoding.UTF8.GetBytes(secretKey),不能直接传secretKey字符串 - 待签名原文处理:
Encoding.UTF8.GetBytes(stringToSign),哪怕原文全是 ASCII 也要显式编码 - 特别注意:从配置文件读取密钥时,确保没带 BOM、换行或不可见控制字符;建议用
secretKey.Trim()预处理 - 若密钥本身是 Base64 编码(如华为云
deviceSecret),先用Convert.FromBase64String解码成 byte[],再喂给 HMAC
签名输出格式要和验签方完全一致
Base64 和十六进制是两种常见输出,混用会导致验证失败。关键不是“哪种好”,而是“两边必须一样”。
- 推荐用
Convert.ToBase64String(hash)——这是大多数云平台(华为云 IoT、阿里云 API 网关)默认格式 - 如果对方要求小写十六进制(如
5f4dcc3b...),用BitConverter.ToString(hash).Replace("-", "").ToLower() - 验签时,收到的
sign字段必须先反向解码:Convert.FromBase64String(receivedSign)得到 byte[],再跟本地计算结果比对 - 绝对不要用字符串比较
==验证签名——必须用CryptographicOperations.FixedTimeEquals(computed, received)(.NET 5+)防时序攻击
验签前必须做时间戳和 nonce 校验
只比对签名是裸奔。攻击者截获一次有效请求,稍作延时重放,签名依然合法。
- 检查
timestamp是否在允许窗口内(如 ±5 分钟),用DateTimeOffset.UtcNow.ToUnixTimeSeconds()计算当前秒级时间戳 - 检查
nonce是否已存在于 Redis 或内存缓存中(TTL 设为略大于时间窗口,如 310 秒),存在则拒绝 - 这两个校验必须在 HMAC 验证之前执行——避免无效请求消耗 CPU 做无意义哈希计算
- 错误响应统一返回
401 Unauthorized,不区分是时间超限、nonce 重复还是签名错,防止信息泄露
最常被跳过的其实是参数拼接前的标准化:GET 和 POST 的参数来源不同(Query vs Body),但签名原文构造逻辑必须统一;很多团队卡在“POST 能过、GET 过不了”,往往是因为 GET 参数漏了 timestamp 或没对空值做显式拼接。


















