C#解析含DateTimeOffset的JSON时哈希验证失败求助
解决Webhook验证中DateTime序列化时区格式不匹配问题
问题背景
接收第三方Webhook请求时,JSON中的DateTime字段采用2023-04-05T14:13:47+0200格式(时区部分无冒号)。使用C# Newtonsoft.Json反序列化后再重新序列化时,时区部分会自动变为+02:00(添加冒号),导致SHA256哈希计算结果与第三方提供的validation_hash不匹配,无法通过请求验证。当前仅能手动修改字段格式,需更优解决方案。
原始请求JSON示例
{ "message": "OK", "code": 200, "current_time": "2023-04-05T14:13:50+0200", "order": { "uuid": "45B32B3C-B01A-4D82-A03B-D5D9709DC92C", "created": "2023-04-05T14:13:47+0200", "created_from_client_timezone": "2023-04-05T14:13:47+0200", "amount": 1000, "currency": "978", "paid": false, "status": "CREATED", "safe": true, "refunded": 0, "additional": null, "service": "REDSYSPSD2", "service_uuid": "F9D76AE8-DDBB-4F17-8074-C2DABEE0A919", "customer": "test2222", "cof_txnid": null, "transactions": [], "token": "345e77cc5a2616030ef5965d5513e1088adbd087b637da99eefc80f0729c943a26ef5e3a45b454819c62015e3d74f3282ecd2e1ab0b99a34de46d8aa37106a3d", "ip": null, "urls": { "payment_card":"http:\/\/ws.paylands.loc\/su\/QY6cnT3MasUB", "bizum":"http:\/\/ws.paylands.loc\/su\/0SWxTAvN0QkJ", "3ds_tokenized":"http:\/\/ws.paylands.loc\/su\/3wEDqxIhjjsD" }, "reference": null, "dynamic_descriptor": "\u00eb\u00f1a,\u00e1ds,\u00f4 \u00f2,", "threeds_data": null }, "client": { "uuid":"42B8CF56-A7D7-4D4A-8349-4E27263CB2D5" }, "validation_hash": "414c8190e6b15ac767ff3217e994ef8687368ec8ebe6ab9ad570799d2f746cb7" }
原哈希计算代码
var jsonDe = JsonConvert.DeserializeObject<dynamic>(requestBody); var signatureData = JsonConvert.SerializeObject(new Dictionary<string, object> { { "order", jsonDe.order }, { "client", jsonDe.client }, }); var signature = CalculateSHA256Hash(signatureData + settings.Signature); if (signature != dto.ValidationHash) { serviceResult.AddError(0, _localizationRepository.Get("The request is invalid.")); return serviceResult; } static string CalculateSHA256Hash(string input) { using (SHA256 sha256Hash = SHA256.Create()) { byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(input)); StringBuilder builder = new StringBuilder(); for (int i = 0; i < bytes.Length; i++) { builder.Append(bytes[i].ToString("x2")); } return builder.ToString(); } }
解决方案
方案1:直接操作原始JSON字符串提取字段(推荐)
绕过反序列化DateTime的步骤,直接从原始JSON中提取order和client节点的字符串片段,拼接后计算哈希,完全保留原始时间格式。
var rawJObject = JObject.Parse(requestBody); // 直接拼接order和client的原始JSON字符串 var signatureData = $"{{\"order\":{rawJObject["order"].ToString()},\"client\":{rawJObject["client"].ToString()}}}"; var signature = CalculateSHA256Hash(signatureData + settings.Signature);
优势:无需处理任何类型转换,完全复用第三方原始数据格式,从根源避免序列化导致的格式变更,性能最优。
方案2:自定义DateTime转换器强制格式
如果需要将JSON映射为强类型对象,可自定义JsonConverter,让DateTime/DateTimeOffset序列化时输出无冒号的时区格式。
自定义转换器代码
public class CustomDateTimeOffsetConverter : JsonConverter<DateTimeOffset> { // 匹配第三方的时间格式 private const string TimeFormat = "yyyy-MM-ddTHH:mm:sszzzz"; public override void WriteJson(JsonWriter writer, DateTimeOffset value, JsonSerializer serializer) { writer.WriteValue(value.ToString(TimeFormat)); } public override DateTimeOffset ReadJson(JsonReader reader, Type objectType, DateTimeOffset existingValue, bool hasExistingValue, JsonSerializer serializer) { return DateTimeOffset.Parse(reader.Value.ToString()); } } // 若使用DateTime类型,需对应实现DateTime的转换器 public class CustomDateTimeConverter : JsonConverter<DateTime> { private const string TimeFormat = "yyyy-MM-ddTHH:mm:sszzzz"; public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer) { writer.WriteValue(value.ToString(TimeFormat)); } public override DateTime ReadJson(JsonReader reader, Type objectType, DateTime existingValue, bool hasExistingValue, JsonSerializer serializer) { return DateTime.Parse(reader.Value.ToString()); } }
序列化时应用转换器
var jsonDe = JsonConvert.DeserializeObject<dynamic>(requestBody); var signatureData = JsonConvert.SerializeObject(new Dictionary<string, object> { { "order", jsonDe.order }, { "client", jsonDe.client }, }, new JsonSerializerSettings { Converters = new List<JsonConverter> { new CustomDateTimeOffsetConverter(), new CustomDateTimeConverter() // 若有DateTime类型字段需添加 } }); var signature = CalculateSHA256Hash(signatureData + settings.Signature);
优势:适配强类型对象场景,确保序列化输出格式符合要求。
方案3:保留JToken原始类型序列化
使用JObject解析后,通过DeepClone保留节点的原始类型(时间字段以字符串形式存储而非DateTime),再序列化时输出原始格式。
var rawJObject = JObject.Parse(requestBody); var signatureObject = new JObject { ["order"] = rawJObject["order"].DeepClone(), ["client"] = rawJObject["client"].DeepClone() }; // 无格式缩进确保和第三方JSON结构一致 var signatureData = signatureObject.ToString(Formatting.None); var signature = CalculateSHA256Hash(signatureData + settings.Signature);
优势:既可以操作JSON节点,又能保留原始格式,适合需要对节点做简单修改的场景。
内容的提问来源于stack exchange,提问作者kamal hamidi
相关产品推荐
相关产品推荐

