问题
这里的基本问题是JSON和ISerializable
+ SerializationInfo
之间的不兼容性,因为它们最初是设计用于与BinaryFormatter
一起使用的,而该格式的流是强类型的。也就是说,ISerializable
的实现有时候会期望序列化流包含序列化字段的完整类型信息。而事实证明XmlException
也有这样一个实现。
具体如下。当Json.NET调用
序列化构造函数来处理一个
ISerializable
类型时,它会
构造一个SerializationInfo
并传递一个
JsonFormatterConverter
,该转换器应该在调用
SerializationInfo.GetValue(String, Type)
时处理从JSON数据到所需类型的转换工作。现在,当未找到命名值时,此方法会抛出异常。不幸的是,
没有SerializationInfo.TryGetValue()
方法,需要反序列化可选字段的类必须通过
GetEnumerator()
手动遍历属性。但是,另外还
没有方法可以检索在构造函数中设置的转换器,这意味着可选字段不能在需要时进行转换,因此必须已经使用完全符合预期类型的方式进行反序列化。
你可以在
XmlException
构造函数的参考源代码中看到这一点:
protected XmlException(SerializationInfo info, StreamingContext context) : base(info, context) {
res = (string) info.GetValue("res" , typeof(string));
args = (string[])info.GetValue("args", typeof(string[]));
lineNumber = (int) info.GetValue("lineNumber", typeof(int));
linePosition = (int) info.GetValue("linePosition", typeof(int));
sourceUri = string.Empty;
string version = null;
foreach ( SerializationEntry e in info ) {
switch ( e.Name ) {
case "sourceUri":
sourceUri = (string)e.Value;
break;
case "version":
version = (string)e.Value;
break;
}
}
事实证明,此时的
e.Value
仍然是
JValue
而不是
string
,因此反序列化失败。
Json.NET可以通过在JsonSerializerInternalReader.CreateISerializable()
中替换字符串类型的JValue
标记为实际字符串来解决这个特定问题,在构造其SerializationInfo
时,然后在JsonFormatterConverter
中重新转换为JValue
(如果需要转换)。但是,这不会解决这种类别的问题。例如,当int
通过Json.NET进行往返传输时,它会变成long
,如果没有转换就进行强制转换将抛出异常。当然,如果没有转换,DateTime
字段也会抛出异常。这还会是一项破坏性的更改,因为之前手动设计用于与Json.NET配合使用的ISerializable
类可能会失效。
我觉得你可以报告这个问题,但我怀疑它不会很快得到解决。
解决问题的更可靠方法是创建一个自定义 JsonConverter
,该转换器嵌入了ISerializable
类型的完整类型信息。
解决方案1:嵌入二进制数据
第一个、最简单的解决方案是在JSON中嵌入BinaryFormatter
流。 Exception
类的序列化代码最初设计为与BinaryFormatter
兼容,因此这应该是相当可靠的:
public class BinaryConverter<T> : JsonConverter where T : ISerializable
{
class BinaryData
{
public byte[] binaryData { get; set; }
}
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var data = serializer.Deserialize<BinaryData>(reader);
if (data == null || data.binaryData == null)
return null;
return BinaryFormatterHelper.FromByteArray<T>(data.binaryData);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var data = new BinaryData { binaryData = BinaryFormatterHelper.ToByteArray(value) };
serializer.Serialize(writer, data);
}
}
public static class BinaryFormatterHelper
{
public static byte [] ToByteArray<T>(T obj)
{
using (var stream = new MemoryStream())
{
new BinaryFormatter().Serialize(stream, obj);
return stream.ToArray();
}
}
public static T FromByteArray<T>(byte[] data)
{
return FromByteArray<T>(data, null);
}
public static T FromByteArray<T>(byte[] data, BinaryFormatter formatter)
{
using (var stream = new MemoryStream(data))
{
formatter = (formatter ?? new BinaryFormatter());
var obj = formatter.Deserialize(stream);
if (obj is T)
return (T)obj;
return default(T);
}
}
}
然后使用以下设置进行序列化:
var settings = new JsonSerializerSettings { Converters = new[] { new BinaryConverter<Exception>() } }
缺点如下:
反序列化不受信任的数据存在严重的安全隐患。由于类型信息完全嵌入专有的、不可读的序列化流中,因此在构造之前无法知道要构造什么。
JSON 是完全不可读的。
我认为某些 .Net 版本中缺少 BinaryFormatter
。
我认为只能在完全信任的情况下使用 BinaryFormatter
。
但如果你所要做的仅仅是在你控制下的进程之间序列化异常,那么这可能已经足够了。
解决方案 2:使用 TypeNameHandling
嵌入类型信息。
Json.NET还具有可选功能,可以通过将
JsonSerializer.TypeNameHandling
设置为适当的值,在序列化流中嵌入非原始类型的.NET类型信息。使用这种能力以及基本类型的包装器,可以创建一个
JsonConverter
,它封装了
SerializationInfo
和
SerializationEntry
并包含所有已知的类型信息:
public class ISerializableConverter<T> : JsonConverter where T : ISerializable
{
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var oldTypeNameHandling = serializer.TypeNameHandling;
var oldAssemblyFormat = serializer.TypeNameAssemblyFormat;
try
{
if (serializer.TypeNameHandling == TypeNameHandling.None)
serializer.TypeNameHandling = TypeNameHandling.Auto;
else if (serializer.TypeNameHandling == TypeNameHandling.Arrays)
serializer.TypeNameHandling = TypeNameHandling.All;
var data = serializer.Deserialize<SerializableData>(reader);
var type = data.ObjectType;
var info = new SerializationInfo(type, new FormatterConverter());
foreach (var item in data.Values)
info.AddValue(item.Key, item.Value.ObjectValue, item.Value.ObjectType);
var value = Activator.CreateInstance(type, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { info, serializer.Context }, serializer.Culture);
if (value is IObjectReference)
value = ((IObjectReference)value).GetRealObject(serializer.Context);
return value;
}
finally
{
serializer.TypeNameHandling = oldTypeNameHandling;
serializer.TypeNameAssemblyFormat = oldAssemblyFormat;
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var oldTypeNameHandling = serializer.TypeNameHandling;
var oldAssemblyFormat = serializer.TypeNameAssemblyFormat;
try
{
var serializable = (ISerializable)value;
var context = serializer.Context;
var info = new SerializationInfo(value.GetType(), new FormatterConverter());
serializable.GetObjectData(info, context);
var data = SerializableData.CreateData(info, value.GetType());
if (serializer.TypeNameHandling == TypeNameHandling.None)
serializer.TypeNameHandling = TypeNameHandling.Auto;
else if (serializer.TypeNameHandling == TypeNameHandling.Arrays)
serializer.TypeNameHandling = TypeNameHandling.All;
serializer.TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Full;
serializer.Serialize(writer, data, typeof(SerializableData));
}
finally
{
serializer.TypeNameHandling = oldTypeNameHandling;
serializer.TypeNameAssemblyFormat = oldAssemblyFormat;
}
}
}
abstract class SerializableValue
{
[JsonIgnore]
public abstract object ObjectValue { get; }
[JsonIgnore]
public abstract Type ObjectType { get; }
public static SerializableValue CreateValue(SerializationEntry entry)
{
return CreateValue(entry.ObjectType, entry.Value);
}
public static SerializableValue CreateValue(Type type, object value)
{
if (value == null)
{
if (type == null)
throw new ArgumentException("type and value are both null");
return (SerializableValue)Activator.CreateInstance(typeof(SerializableValue<>).MakeGenericType(type));
}
else
{
type = value.GetType();
return (SerializableValue)Activator.CreateInstance(typeof(SerializableValue<>).MakeGenericType(type), value);
}
}
}
sealed class SerializableValue<T> : SerializableValue
{
public SerializableValue() : base() { }
public SerializableValue(T value)
: base()
{
this.Value = value;
}
public override object ObjectValue { get { return Value; } }
public override Type ObjectType { get { return typeof(T); } }
[JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)]
public T Value { get; private set; }
}
abstract class SerializableData
{
public SerializableData()
{
this.Values = new Dictionary<string, SerializableValue>();
}
public SerializableData(IEnumerable<SerializationEntry> values)
{
this.Values = values.ToDictionary(v => v.Name, v => SerializableValue.CreateValue(v));
}
[JsonProperty("values", ItemTypeNameHandling = TypeNameHandling.Auto)]
public Dictionary<string, SerializableValue> Values { get; private set; }
[JsonIgnore]
public abstract Type ObjectType { get; }
public static SerializableData CreateData(SerializationInfo info, Type initialType)
{
if (info == null)
throw new ArgumentNullException("info");
var type = info.GetSavedType(initialType);
if (type == null)
throw new InvalidOperationException("type == null");
return (SerializableData)Activator.CreateInstance(typeof(SerializableData<>).MakeGenericType(type), info.AsEnumerable());
}
}
sealed class SerializableData<T> : SerializableData
{
public SerializableData() : base() { }
public SerializableData(IEnumerable<SerializationEntry> values) : base(values) { }
public override Type ObjectType { get { return typeof(T); } }
}
public static class SerializationInfoExtensions
{
public static IEnumerable<SerializationEntry> AsEnumerable(this SerializationInfo info)
{
if (info == null)
throw new NullReferenceException();
var enumerator = info.GetEnumerator();
while (enumerator.MoveNext())
{
yield return enumerator.Current;
}
}
public static Type GetSavedType(this SerializationInfo info, Type initialType)
{
if (initialType != null)
{
if (info.FullTypeName == initialType.FullName
&& info.AssemblyName == initialType.Module.Assembly.FullName)
return initialType;
}
var assembly = Assembly.Load(info.AssemblyName);
if (assembly != null)
{
var type = assembly.GetType(info.FullTypeName);
if (type != null)
return type;
}
return initialType;
}
}
然后使用以下设置:
这将生成类似以下的半可读JSON:
{
"$type": "Question35015357.SerializableData`1[[System.Xml.XmlException, System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"values": {
"ClassName": {
"$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"value": "System.Xml.XmlException"
},
"Message": {
"$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"value": "bla"
},
"Data": {
"$type": "Question35015357.SerializableValue`1[[System.Collections.IDictionary, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
},
"InnerException": {
"$type": "Question35015357.SerializableValue`1[[System.Exception, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
},
"HelpURL": {
"$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
},
"StackTraceString": {
"$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
},
"RemoteStackTraceString": {
"$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
},
"RemoteStackIndex": {
"$type": "Question35015357.SerializableValue`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"value": 0
},
"ExceptionMethod": {
"$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
},
"HResult": {
"$type": "Question35015357.SerializableValue`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"value": -2146232000
},
"Source": {
"$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
},
"res": {
"$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"value": "Xml_UserException"
},
"args": {
"$type": "Question35015357.SerializableValue`1[[System.String[], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"value": [
"bla"
]
},
"lineNumber": {
"$type": "Question35015357.SerializableValue`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"value": 0
},
"linePosition": {
"$type": "Question35015357.SerializableValue`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"value": 0
},
"sourceUri": {
"$type": "Question35015357.SerializableValue`1[[System.Object, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
},
"version": {
"$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"value": "2.0"
}
}
}
正如您所看到的,JSON 的可读性在一定程度上减轻了安全隐患。您还可以创建一个
自定义SerializationBinder
,以进一步减少安全隐患,仅加载预期类型,如在
Newtonsoft Json中的TypeNameHandling注意事项中所述。
我不确定部分信任情况下应该做什么。
JsonSerializerInternalReader.CreateISerializable()
在部分信任中抛出异常:
private object CreateISerializable(JsonReader reader, JsonISerializableContract contract, JsonProperty member, string id)
{
Type objectType = contract.UnderlyingType;
if (!JsonTypeReflector.FullyTrusted)
{
string message = @"Type '{0}' implements ISerializable but cannot be deserialized using the ISerializable interface because the current application is not fully trusted and ISerializable can expose secure data." + Environment.NewLine +
@"To fix this error either change the environment to be fully trusted, change the application to not deserialize the type, add JsonObjectAttribute to the type or change the JsonSerializer setting ContractResolver to use a new DefaultContractResolver with IgnoreSerializableInterface set to true." + Environment.NewLine;
message = message.FormatWith(CultureInfo.InvariantCulture, objectType);
throw JsonSerializationException.Create(reader, message);
}
也许转换器也应该这样做。