ASP.NET Core 2控制器中编写自定义JsonConverter处理POST多态JSON反序列化
实现安全的多态JSON反序列化自定义JsonConverter(ASP.NET MVC Core 2)
我之前在ASP.NET MVC Core 2项目里刚好解决过一模一样的问题——既要处理多态类型数组的反序列化,又得避开TypeNameHandling的安全风险,下面是我实战验证过的完整方案,完全适配默认模型绑定:
核心思路
我们不用自带的TypeNameHandling,而是手动在JSON数据里携带可控的类型标识,同时维护一个类型白名单,只允许我们预先定义的类型被反序列化,从根源上避免安全风险。
1. 定义类型约定与白名单
首先我们约定每个多态对象必须携带一个typeDiscriminator字段,值是我们预定义的类型别名,再用字典把别名映射到具体类型:
// 类型鉴别器的常量定义,统一管理字段名和允许的类型 public static class TypeDiscriminatorConstants { public const string Key = "typeDiscriminator"; // 白名单:只允许这里列出的类型被反序列化 public static readonly Dictionary<string, Type> AllowedTypes = new Dictionary<string, Type> { { "Car", typeof(Car) }, { "Truck", typeof(Truck) }, { "Motorcycle", typeof(Motorcycle) } }; } // 基类和派生类示例 public abstract class Vehicle { public string Model { get; set; } } public class Car : Vehicle { public int NumberOfDoors { get; set; } } public class Truck : Vehicle { public int LoadCapacity { get; set; } } public class Motorcycle : Vehicle { public bool HasSideCar { get; set; } }
2. 编写自定义JsonConverter
这个转换器专门处理Vehicle[]类型的反序列化,核心逻辑是:读取每个对象的类型标识,从白名单找到对应类型,再反序列化:
public class PolymorphicVehicleConverter : JsonConverter<Vehicle[]> { public override Vehicle[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartArray) { throw new JsonException("请求体必须是数组格式"); } var vehicleList = new List<Vehicle>(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndArray) break; // 先把当前对象解析为JsonDocument,方便提取类型标识 using var doc = JsonDocument.ParseValue(ref reader); var rootElement = doc.RootElement; // 检查是否包含必填的类型标识字段 if (!rootElement.TryGetProperty(TypeDiscriminatorConstants.Key, out var typeAliasElement)) { throw new JsonException($"缺少必填字段:{TypeDiscriminatorConstants.Key}"); } var typeAlias = typeAliasElement.GetString(); // 验证类型是否在白名单内 if (!TypeDiscriminatorConstants.AllowedTypes.TryGetValue(typeAlias, out var targetType)) { throw new JsonException($"不允许的类型:{typeAlias}"); } // 反序列化为目标类型并加入列表 var vehicle = (Vehicle)JsonSerializer.Deserialize(rootElement.GetRawText(), targetType, options); vehicleList.Add(vehicle); } return vehicleList.ToArray(); } public override void Write(Utf8JsonWriter writer, Vehicle[] value, JsonSerializerOptions options) { // 如果需要序列化多态数组返回给前端,这里补充写入类型标识的逻辑 writer.WriteStartArray(); foreach (var vehicle in value) { writer.WriteStartObject(); // 写入类型标识 var typeAlias = TypeDiscriminatorConstants.AllowedTypes.First(kvp => kvp.Value == vehicle.GetType()).Key; writer.WriteString(TypeDiscriminatorConstants.Key, typeAlias); // 写入对象的其他属性 using var doc = JsonDocument.Parse(JsonSerializer.Serialize(vehicle, options)); foreach (var property in doc.RootElement.EnumerateObject()) { if (property.Name != TypeDiscriminatorConstants.Key) { property.WriteTo(writer); } } writer.WriteEndObject(); } writer.WriteEndArray(); } }
3. 注册转换器到MVC Core 2的默认模型绑定
在Startup.cs的ConfigureServices方法里,把自定义转换器添加到JSON序列化配置中,这样模型绑定时就会自动使用它:
public void ConfigureServices(IServiceCollection services) { services.AddMvc() .AddJsonOptions(options => { // 添加自定义多态转换器 options.SerializerOptions.Converters.Add(new PolymorphicVehicleConverter()); // 关闭默认的类型解析,确保安全 options.SerializerOptions.TypeInfoResolver = null; }); }
4. 前端jQuery POST请求示例
确保前端发送的JSON数组中,每个对象都包含typeDiscriminator字段:
var vehiclesData = [ { typeDiscriminator: "Car", model: "特斯拉Model 3", numberOfDoors: 4 }, { typeDiscriminator: "Truck", model: "福特F-150", loadCapacity: 1000 } ]; $.ajax({ url: "/Vehicle/AddVehicles", type: "POST", contentType: "application/json", data: JSON.stringify(vehiclesData), success: function(res) { console.log("提交成功!", res); }, error: function(err) { console.error("提交失败:", err); } });
5. 控制器Action示例
此时模型绑定会自动把请求体的JSON数组反序列化为正确的派生类型数组:
public class VehicleController : Controller { [HttpPost] public IActionResult AddVehicles([FromBody] Vehicle[] vehicles) { foreach (var vehicle in vehicles) { switch (vehicle) { case Car car: // 处理轿车类型,比如获取NumberOfDoors Console.WriteLine($"收到轿车:{car.Model},车门数:{car.NumberOfDoors}"); break; case Truck truck: // 处理卡车类型,比如获取LoadCapacity Console.WriteLine($"收到卡车:{truck.Model},载重:{truck.LoadCapacity}kg"); break; case Motorcycle moto: // 处理摩托车类型 Console.WriteLine($"收到摩托车:{moto.Model},带边车:{moto.HasSideCar}"); break; } } return Ok("处理完成"); } }
关键注意事项
- 安全保障:通过
AllowedTypes白名单,完全杜绝了TypeNameHandling可能带来的任意类型注入攻击,只有我们预先认可的类型才能被反序列化。 - 扩展性:如果需要新增派生类型,只需要在
AllowedTypes字典里添加对应的别名和类型即可,无需修改转换器核心代码。 - 兼容性:这个方案适配ASP.NET MVC Core 2默认的
System.Text.Json序列化器,如果你的项目用的是Newtonsoft.Json,转换器的写法会略有不同,但核心思路一致。
内容的提问来源于stack exchange,提问作者James Morrison
相关产品推荐
相关产品推荐

