C# System.Text.Json序列化时如何实现参数字典序排序生成有效签名
问题:强类型请求类生成交易所签名时缺失参数字典序排序导致校验失败
- 核心根因:交易所签名鉴权的通用要求是签名原串拼接前,所有请求参数必须按参数名的ASCII字典序升序排列。直接用强类型类配合
System.Text.Json默认序列化时,属性输出顺序和类定义顺序一致,不满足字典序要求,因此签名校验失败。 SortedDictionary方案可正常运行的本质,是容器自动完成了键的字典序排序,不需要手动维护参数顺序。
存在问题的错误实现(强类型直接序列化方案)
// 强类型请求载荷 public class PlaceOrderPayload { public string Symbol { get; set; } public string Side { get; set; } public decimal Quantity { get; set; } public long Timestamp { get; set; } public decimal Price { get; set; } } // 问题签名逻辑:直接序列化未做参数排序 public string GenerateInvalidSignature(PlaceOrderPayload payload, string apiSecret) { // 序列化输出顺序为类定义顺序:Symbol -> Side -> Quantity -> Timestamp -> Price // 不符合接口要求的字典序:Price -> Quantity -> Side -> Symbol -> Timestamp var paramJson = JsonSerializer.Serialize(payload); using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(paramJson)); return Convert.ToHexString(hash).ToLower(); }
可正常运行的参考实现(SortedDictionary方案)
public string GenerateValidSignatureWithSortedDict(Dictionary<string, object> @params, string apiSecret) { // SortedDictionary默认按key的字符串字典序升序排列 var sortedParams = new SortedDictionary<string, object>(@params, StringComparer.Ordinal); var paramStr = string.Join("&", sortedParams.Select(kv => $"{kv.Key}={kv.Value}")); using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(paramStr)); return Convert.ToHexString(hash).ToLower(); }
第一种强类型方案的修改方法
不需要放弃强类型载荷的简洁写法,只需要在生成签名原串的环节,自动完成强类型对象属性的字典序排序即可,不需要改动原有业务层的载荷定义逻辑。
修复逻辑
- 反射读取强类型对象的所有公共实例属性,过滤值为null的属性(匹配绝大多数交易所的参数过滤规则)
- 按属性名的ASCII字节序做升序排列(和交易所Java侧默认排序规则完全对齐)
- 根据接口要求的签名原串格式(JSON格式/Query拼接格式),按排序后的属性顺序生成原串
- 走原有HMAC签名逻辑即可
修改后可直接复用的代码
using System.Reflection; using System.Text.Json; using System.Security.Cryptography; using System.Text; public static class ExchangeSignatureHelper { /// <summary> /// 从强类型对象生成按参数名字典序排序的签名原串 /// </summary> private static string BuildSortedSignContent<T>(T payload, bool useJsonFormat = false) { if (payload == null) throw new ArgumentNullException(nameof(payload)); // 读取属性、过滤空值、按ASCII字典序排序 var sortedProps = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.GetValue(payload) != null) .OrderBy(p => p.Name, StringComparer.Ordinal) .ToList(); if (useJsonFormat) { // 适配要求JSON格式原串的接口 var sortedKv = new Dictionary<string, object>(); foreach (var prop in sortedProps) { sortedKv.Add(prop.Name, prop.GetValue(payload)); } return JsonSerializer.Serialize(sortedKv); } // 适配要求key=value&key=value格式原串的接口 return string.Join("&", sortedProps.Select(p => $"{p.Name}={Uri.EscapeDataString(p.GetValue(payload)?.ToString() ?? string.Empty)}")); } /// <summary> /// 支持强类型载荷的正确签名方法 /// </summary> public static string Sign<T>(T payload, string apiSecret, bool useJsonFormat = false) { var signContent = BuildSortedSignContent(payload, useJsonFormat); using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret)); var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(signContent)); return Convert.ToHexString(hashBytes).ToLower(); } } // 业务层调用示例,不需要改动原有强类型定义 public void TestPlaceOrder() { var orderPayload = new PlaceOrderPayload { Symbol = "BTCUSDT", Side = "BUY", Quantity = 0.01m, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Price = 50000m }; // 直接传入强类型对象,内部自动完成参数排序 var sign = ExchangeSignatureHelper.Sign(orderPayload, "你的API密钥", useJsonFormat: false); }
注意事项
- 排序必须使用
StringComparer.Ordinal做严格ASCII字节序比较,不要用当前区域的字符串比较规则,否则会和交易所Java服务端的排序结果不一致 - 如果接口要求嵌套对象参与签名,需要递归处理嵌套对象内部的属性排序,绝大多数交易所私有接口的签名参数为平铺结构,不需要递归
- 注意对齐接口的参数值格式要求:比如Decimal类型不要保留多余尾随0、时间戳为秒级/毫秒级、特殊字符是否需要URL编码
内容的提问来源于stack exchange,提问作者nop
相关产品推荐
相关产品推荐

