You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
}

第一种强类型方案的修改方法

不需要放弃强类型载荷的简洁写法,只需要在生成签名原串的环节,自动完成强类型对象属性的字典序排序即可,不需要改动原有业务层的载荷定义逻辑。

修复逻辑

  1. 反射读取强类型对象的所有公共实例属性,过滤值为null的属性(匹配绝大多数交易所的参数过滤规则)
  2. 按属性名的ASCII字节序做升序排列(和交易所Java侧默认排序规则完全对齐)
  3. 根据接口要求的签名原串格式(JSON格式/Query拼接格式),按排序后的属性顺序生成原串
  4. 走原有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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 20:06:10