HMACSHA256签名需确保key和data均为UTF-8字节数组,验证时用CryptographicOperations.FixedTimeEquals恒定时间比较,禁用null或空key,跨语言对接须统一编码、key处理及输出格式。

生成 HMACSHA256 签名时 key 和 data 必须是字节数组,不能直接传字符串
常见错误是把 key 或 data 当成纯字符串直接喂给 HMACSHA256.ComputeHash,结果签名总对不上。.NET 的 HMACSHA256 只接受 byte[],必须显式编码。默认用 UTF-8,但要和验签端严格一致(比如对方用 ASCII,你就不能用 UTF-8)。
实操建议:
- 统一用
Encoding.UTF8.GetBytes("your-key")转 key 和待签名原文 - 如果原文含 Base64、URL 编码等,先解码再签名,别在编码后签名
- 签名前确认原文无隐藏空格、BOM、换行符——尤其从文件或 HTTP body 读取时
- 生成的
byte[]签名通常转为十六进制字符串(BitConverter.ToString(hash).Replace("-", "").ToLower())或 Base64(Convert.ToBase64String(hash)),选哪种取决于对接方要求
验证签名时不能只比对字符串,要防时序攻击
直接用 == 或 String.Equals 比较两个签名字符串,会暴露时间差异,可能被用于侧信道攻击。.NET 没有内置恒定时间比较字符串的方法,得自己写或用现成安全库。
实操建议:
- 用
CryptographicOperations.FixedTimeEquals(.NET 5+)做字节级恒定时间比较:CryptographicOperations.FixedTimeEquals(computedHash, receivedHash) - 如果还在用 .NET Framework 或 .NET Core 3.1,手写恒定时间比较:遍历每个字节异或再或运算,最后判断结果是否全 0
- 验证前务必把收到的签名(如 Base64 字符串)先解码为
byte[],再和本地计算出的byte[]比较,不要比字符串 - 验证失败时统一返回 401,不要透露是密钥错、原文错还是签名格式错
HMACSHA256 构造函数传 null key 会抛 CryptographicException
代码里写 new HMACSHA256(null) 看似省事,实际运行直接崩,报错信息是 CryptographicException: Object synchronization method was called from an unsynchronized block of code(误导性很强)。根本原因是 null 导致内部 key 初始化失败,并发访问时触发同步异常。
实操建议:
- key 必须是非 null、非空的
byte[];空 key(长度为 0)也不行,会报ArgumentException: Key must be at least 64 bits long - 生产环境 key 应从配置中心或 Azure Key Vault 加载,避免硬编码;本地调试可用
Convert.FromBase64String("...")安全导入 - 每次 new 一个新
HMACSHA256实例,别复用——它不是线程安全的,且内部状态会变
跨语言验签失败?重点查这三处编码和填充
和 Python/Java/Node.js 对接时签名总不一致,90% 出在以下三点:
- 原文编码:Python 默认 str 是 Unicode,
hmac.new(key.encode(), data.encode(), 'sha256')中encode()默认是 UTF-8;C# 也必须用Encoding.UTF8.GetBytes,不能用ASCIIEncoding - key 处理:Java 的
SecretKeySpec接收 byte[],但如果 key 是字符串,需确保两边都按相同方式转字节(比如都不额外 Base64 解码) - 输出格式:对方返回的是 hex 小写?大写?带冒号?还是 Base64?C# 生成后要用对应方式格式化,别用
ToString()直接打日志看——它输出的是类型名
最稳妥的调试方式:两边都打印原始 byte[] 的十六进制(例如 C# 用 BitConverter.ToString(hash).Replace("-", "")),逐字节比对。










