Skip to content
Go back

.NET Framework 4.6.2 安全深拷贝实现方案研究

Published:  at  12:00 AM

.NET Framework 4.6.2 安全深拷贝实现方案研究

深拷贝(deep clone)指的是创建一个对象的完整副本,包括其包含的所有子对象,从而使原对象和副本在内存中完全独立。实现深拷贝时,安全性是首要考虑因素,需要避免使用已知存在反序列化安全漏洞的技术(例如 BinaryFormatter) (Deserialization risks in use of BinaryFormatter and related types - .NET | Microsoft Learn)。本文将评估多种适用于 .NET Framework 4.6.2 的安全深拷贝实现方式,包括使用 JSON 序列化(Newtonsoft.Json)、使用 Protobuf-net 序列化、利用表达式树或反射的通用实现,以及使用 AutoMapper 映射来克隆对象。每种方法都会从安全性、开源许可证、性能和适用性等方面进行分析,并给出示例。

深拷贝的安全性考量

在讨论具体实现方案之前,需要明确为什么像 BinaryFormatter 这样的传统深拷贝手段是不安全的。Microsoft 明确指出:BinaryFormatter.Deserialize 无法保证安全,不应在任何场景下使用(即使反序列化的数据被认为是可信的) (Deserialization risks in use of BinaryFormatter and related types - .NET | Microsoft Learn)。反序列化攻击已成为常见安全威胁,利用像 BinaryFormatter 这样的反序列化器,恶意输入可以触发任意代码执行、操纵程序流程或导致崩溃 (Do not use BinaryFormatter as it is insecure and vulnerable ) (Deserialization risks in use of BinaryFormatter and related types - .NET | Microsoft Learn)。简而言之,调用 BinaryFormatter.Deserialize 等同于执行了输入提供的可执行代码 (Deserialization risks in use of BinaryFormatter and related types - .NET | Microsoft Learn)。因此,我们在实现深拷贝时必须避免使用 BinaryFormatter 及其相关类型(如 SoapFormatterNetDataContractSerializer 等) (Deserialization risks in use of BinaryFormatter and related types - .NET | Microsoft Learn) (Deserialization risks in use of BinaryFormatter and related types - .NET | Microsoft Learn)。

为了替代 BinaryFormatter,有多种更安全的方案可供选择,包括 JSON 序列化、XML 序列化、ProtoBuf、MessagePack 等 (Do not use BinaryFormatter as it is insecure and vulnerable )。在接下来的小节中,我们将重点讨论四种满足安全要求且与 .NET Framework 4.6.2 兼容的深拷贝实现方式,并分析它们的安全性、性能、适用场景和许可证。

基于 JSON(Newtonsoft.Json)的深拷贝

使用 JSON 序列化和反序列化进行深拷贝是一种简单直接的方法。通过将对象序列化为 JSON 字符串,然后再反序列化回对象,可以得到一个全新的对象副本。常用的 Newtonsoft.Json(又称Json.NET)库提供了方便的接口。Newtonsoft.Json 拥有 MIT 开源许可证 (Ionic Newton Jsoft license - PlayFab | Microsoft Learn)(许可证友好),并且在 .NET Framework 4.6.2 上兼容良好。

实现示例

利用 Newtonsoft.Json 实现深拷贝的代码非常简洁。例如,可以编写一个扩展方法:

using Newtonsoft.Json;

public static class ObjectExtensions 
{
    public static T DeepClone<T>(this T source)
    {
        if (source == null) return default(T);
        string json = JsonConvert.SerializeObject(source);
        // 反序列化回对象副本
        return (T)JsonConvert.DeserializeObject(json, source.GetType());
    }
}

调用方式:MyObject clone = originalObject.DeepClone();。上述实现思想与以下示例等价 (c# - Deep cloning objects - Stack Overflow):

MyType clone = (MyType)JsonConvert.DeserializeObject(
                   JsonConvert.SerializeObject(sourceObj), 
                   sourceObj.GetType());

Newtonsoft.Json 会将对象及其子对象全部序列化为文本,再根据 JSON 重建对象树,从而实现深拷贝。

安全性分析

与 BinaryFormatter 不同,JSON 序列化产生纯文本数据,不含可执行payload,因而不存在反序列化导致远程代码执行的已知风险 (Do not use BinaryFormatter as it is insecure and vulnerable )。Microsoft 也将基于 JSON 的序列化列为安全替代方案之一 (Do not use BinaryFormatter as it is insecure and vulnerable )。使用 Newtonsoft.Json 时,只要不启用不安全的类型信息反序列化功能(如不使用 TypeNameHandling 去反序列化多态类型),通常是安全的。默认情况下,上述深拷贝实现并不会在 JSON 中包含类型名,因此不会有通过植入恶意类型来攻击的可能。

需要注意的是,使用 JSON 深拷贝仍需确保对象的数据是可信的,因为反序列化过程会根据 JSON 数据设置对象属性。如果对象中有某些属性的 get/set 有副作用,反序列化时可能触发这些副作用。不过这种风险属于业务逻辑范畴,而非外部攻击。总的来说,基于 JSON 的克隆在安全性方面是可接受的,前提是序列化和反序列化发生在受信任的环境中(例如在内存中克隆内部对象)。

性能与限制

JSON 序列化的主要缺点是性能相对较慢。序列化和反序列化过程涉及大量字符串处理和反射,开销不容忽视 (GitHub - marcelltoth/ObjectCloner: Insanely fast and capable Deep Clone implementation for .NET based on Expression Trees)。有基准测试显示,对同一个对象进行深拷贝,不同方法耗时对比如下 (GitHub - marcelltoth/ObjectCloner: Insanely fast and capable Deep Clone implementation for .NET based on Expression Trees):

可见,JSON 方法在性能上明显落后于直接内存拷贝方案。如果深拷贝操作在应用中非常频繁或对象规模很大,那么JSON方法可能成为瓶颈。

除了速度,JSON 克隆还存在以下限制:

适用场景总结

Newtonsoft.Json 方法胜在实现简单、对对象几乎零侵入(无需修改类定义或实现特定接口),并且安全性可靠(无原生反序列化漏洞)。它适用于以下场景:

需要高性能或复杂对象图场景下,可能需要考虑下面讨论的其他方案。

基于 Protobuf-net 的深拷贝

Protobuf-net 是 .NET 平台上的 Protocol Buffers(二进制协议)序列化库,由 Marc Gravell 开发和维护。它以高效、小尺寸的二进制格式序列化对象,在网络传输和持久化方面经常被使用。同样地,这种序列化/反序列化过程也可以用于深拷贝对象。Protobuf-net 采用 Apache 2.0 许可证 ( NuGet Gallery | protobuf-net 3.2.30 ),符合友好许可证要求,并且明确支持 .NET Framework 4.6.2 ( NuGet Gallery | protobuf-net 3.2.30 )。

实现方式

使用 protobuf-net 进行深拷贝通常有两种方式:

  1. 使用静态方法 DeepClone:Protobuf-net 提供了内置的深拷贝辅助方法。例如:

    MyType clone = ProtoBuf.Serializer.DeepClone(originalObj);

    该方法会自动将对象序列化为二进制流并立即反序列化出一个新对象,相当于一次高效完成拷贝 (protobuf-net/src/protobuf-net/Serializer.cs at main · protobuf-net/protobuf-net · GitHub)。需要注意,这要求类型 MyType 已被 Protobuf-net 支持(见下文要求)。

  2. 手动序列化再反序列化:等价地,可以先将对象序列化到 MemoryStream,再重置流指针进行反序列化。静态方法 DeepClone 实际上就是封装了这两个步骤,因此通常直接使用 Serializer.DeepClone<T> 最方便。

使用要求和限制

为使 Protobuf-net 正常序列化对象类型,需要满足一定的契约要求

综上,使用 Protobuf-net 需要对类型进行一定程度的预先准备,通过特性标记或建立元数据模型。对于**POCO对象(简单的属性容器)**来说,这种准备工作是直接的;但对于引入第三方类型或复杂对象模型,若无法修改源码添加特性,则需要使用 RuntimeTypeModel 来配置序列化规则,这增加了实现难度。

安全性分析

Protobuf-net 的序列化过程同样不存在类似 BinaryFormatter 的安全漏洞。它将对象严格按照预定义的合同转换为紧凑的字节格式,反序列化时也严格按照合同和目标类型重建对象。因为不嵌入类型元数据,外部输入无法通过操纵数据来实例化任意类或执行任意代码,安全性很高 (Do not use BinaryFormatter as it is insecure and vulnerable )。事实上,微软和安全社区将 Protocol Buffers(以及MessagePack等)视为安全的序列化方案 (Do not use BinaryFormatter as it is insecure and vulnerable )。Protobuf-net 本身经过广泛使用和验证,没有已知RCE漏洞。

需要注意的反而是业务逻辑层面的安全:由于 Protobuf-net 会重建对象,如果对象内部存在一些在属性赋值/字段设置时执行的逻辑,那么克隆出的对象也会执行这些逻辑(类似于JSON情形)。但这不是库的漏洞,而是使用上的考虑。

总而言之,在同等受信任环境中,使用 Protobuf-net 进行深拷贝在安全性上是非常可靠的。

性能与适配性

性能是 Protobuf-net 的一大优势。它采用二进制协议,序列化与反序列化效率都很高,远胜于JSON文本格式。在常见场景下,Protobuf-net 的克隆速度可接近手工复制的性能:

然而,Protobuf-net 的适用性在于你是否需要为了深拷贝去为所有类型增加序列化标记。如果项目本身就有使用 Protobuf-net(例如用于通信或存储),那么复用它来做对象克隆是很自然的选择。如果纯粹为了实现深拷贝而引入 Protobuf-net,权衡点在于:需要为每个要克隆的类型编写契约定义(特性或配置)。当类型很多或类型结构经常变化时,维护这些契约可能成为负担。此时,相比之下,下一节讨论的通用反射/表达式方案可能更合适,因为它不要求预先标注类型信息。

适用场景总结

考虑使用 Protobuf-net 进行深拷贝的场景包括:

反之,如果对象模型无法轻易适配 Protobuf-net(比如大量第三方类或无法修改源代码的类),或者深拷贝只是偶尔需要,项目中也不打算引入额外依赖,那么可以考虑其他方案。

基于表达式树或反射的深拷贝实现

不借助特定数据格式进行序列化,我们也可以通过直接遍历对象图并复制的方式来实现深拷贝。这类方案通常利用反射来获取对象的字段和属性,然后递归地创建副本。为提升性能,可以结合表达式树或IL生成技术,将重复的反射操作转换为高效的委托或动态方法,从而接近手写克隆代码的速度。这一类方法的好处是通用性极强:几乎可以处理任意对象,而无需预先对类进行标注或实现接口。许多第三方库(开源许可证友好)都采用了这种思路,比如 Force.DeepCloner (MIT 许可证) (GitHub - force-net/DeepCloner: Fast object cloner for .NET)、Baksteen.Extensions.DeepCopy (MIT) 等等。

实现原理

基本思想是:对于一个给定对象,创建它的一个新实例,然后逐个将其所有字段/属性值拷贝到新实例。对于值类型或不可变类型,直接赋值即可;对于引用类型,则需要递归克隆其子对象,再赋给副本的相应字段。为了防止循环引用导致无限递归,需要维护一个引用映射表(例如使用 Dictionary<object, object> 记录已经克隆过的对象),每当准备克隆一个新对象时先查表,看是否已经克隆过,是则直接重用已有副本。这确保了在克隆结果中,原本共享同一引用的多个对象依然共享同一引用,从而维护原对象图的拓扑结构

纯反射实现每次深拷贝都要遍历类型的元数据,可能比较慢。于是可引入缓存机制:对于每种类型,第一次克隆时通过反射拿到所有可克隆成员列表,并动态构造一个执行复制的委托(例如使用表达式树编译或 DynamicMethod 生成IL)。后续再克隆相同类型对象时,直接调用缓存的委托即可,大大提升速度 (c# - Deep cloning objects - Stack Overflow)。通过这些手段,深拷贝操作可以做到不调用对象的构造函数、不依赖任何特定特性,仅按照内存中对象布局复制,从而非常高效 (GitHub - force-net/DeepCloner: Fast object cloner for .NET)。

第三方库示例

安全性分析

这种直接克隆法不涉及外部数据输入的解析,因此从输入攻击的角度看是安全的。它不会生成可被篡改的中间表示,更不存在反序列化注入的风险,因为整个过程都在内存中操作受控的对象。需要注意的安全/风险点包括:

综上,表达式树/反射深拷贝方案本身非常安全,没有引入新的外部攻击面。只需在业务层面避免克隆那些“不该克隆”的对象类型即可。

性能分析

经过优化的深拷贝库性能非常出色。以 Force.DeepCloner 等为代表的实现,官方和社区提供的基准都显示其速度与手写克隆代码在同一个量级,远远快于序列化方案 (GitHub - marcelltoth/ObjectCloner: Insanely fast and capable Deep Clone implementation for .NET based on Expression Trees)。前文提到的性能对比也证明了这一点:表达式树方案比 Newtonsoft.Json 快一个数量级以上,足以应对大部分性能要求苛刻的场景。在克隆包含大量数据的对象时,此类库通常只会消耗微秒级别的时间。

当然,不同库的性能略有差异,但一般都做了常见类型(如数组、集合)的特殊优化。例如,有的实现会针对 List<T>、数组等做批量内存复制优化,而不仅仅是逐元素克隆。另外,由于这些库都是零拷贝地直接在内存中复制,GC压力主要来自新对象的分配,没有像JSON那样的大字符串临时对象,所以内存开销相对也更低。

适用场景总结

表达式树/反射法的深拷贝非常通用,几乎适用于任何需要深拷贝的场景,特别是:

相应地,要小心使用在涉及本地资源句柄或单例的对象上,必要时可在克隆前断开这些链接或在克隆后手工调整。整体而言,采用反射/表达式树方案是在 .NET Framework 上实现安全深拷贝的最通用且高性能的方法之一。

使用 AutoMapper 进行深拷贝

AutoMapper 是 .NET 中广为使用的对象-对象映射库,通常用于将一种业务模型转换为另一种(例如 DTO <-> 实体)。虽然 AutoMapper 并非专门为深拷贝设计,但它可以配置为将对象映射到相同类型,从而实现类似深拷贝的效果。AutoMapper 当前版本使用 MIT 许可证开放源代码 (AutoMapper/LICENSE.txt at master · AutoMapper/AutoMapper · GitHub);不过需要注意的是,AutoMapper 作者已宣布计划将其商业化,在未来版本中可能不再是纯开源免费模式 (About the AutoMapper License Change · Issue #22582 · abpframework/abp · GitHub)。在考虑长期使用时,应关注其许可证政策变化。

实现方式

使用 AutoMapper 进行深拷贝的典型步骤:

  1. 创建映射配置:告知AutoMapper如何从类型映射到自身。例如:

    var config = new MapperConfiguration(cfg => {
        cfg.CreateMap<Foo, Foo>();
    });
    IMapper mapper = config.CreateMapper();

    上述配置定义了类型 FooFoo 的映射规则,实际上等同于深拷贝需要的操作(即把源 Foo 的每个成员赋值到目标 Foo)。

  2. 执行映射克隆:有了映射配置,就可以执行:

    Foo source = ...;
    Foo clone = mapper.Map<Foo>(source);

    这会创建一个新的 Foo 对象,并将 source 的可映射成员复制过去。对于复杂对象,AutoMapper会递归映射其属性中的对象。例如,如果 Foo 有一个 Bar 属性(类型为 Bar),且也有 CreateMap<Bar, Bar> 配置,那么 mapper.Map 会进一步克隆 Bar 对象。同理,列表、数组等如果在配置中建立了元素映射,也会被逐一映射克隆。

值得一提的是,AutoMapper 也支持映射到已有对象的重载,比如 mapper.Map(source, existingTarget)。但在深拷贝语义下,我们通常是创建新对象,所以直接用上面的方式更简单。

AutoMapper 深拷贝的行为实际上是浅拷贝属性+递归:它按公开属性进行映射复制,默认情况下只映射公共可读可写属性。私有字段、只读属性不会被处理。这跟 JSON 方法有些类似,但 AutoMapper 可以通过配置自定义映射规则(ForMember等)覆盖默认行为。如果需要,它也可以映射字段但要额外配置。在一般用法下,我们认为AutoMapper克隆的对象和使用DTO类似——即拷贝重要的数据字段,但可能不会完全复制对象的内部隐藏状态。

安全性分析

AutoMapper 在克隆过程中不涉及外部输入,所有数据仍来自源对象本身,因此不存在反序列化漏洞。它只是调用源对象的getter和目标对象的setter来复制值,这跟普通的赋值操作无异。需要关注的安全点包括:

总的来说,AutoMapper 克隆没有额外的安全风险。只要源对象安全,结果也是安全的。此外,AutoMapper 作为成熟库,本身也没有已知安全漏洞。

性能分析

AutoMapper 的性能介于纯反射拷贝和高度优化的表达式树拷贝之间。事实上,AutoMapper 内部也使用表达式树/IL来构建映射委托,以减少每次映射的反射开销。因此对于单次对象拷贝来说,它比纯使用反射逐属性赋值快很多。

不过,与深度定制的深拷贝库相比,AutoMapper 做了更多抽象:它需要解析映射配置,支持各种转换和条件,这些灵活性在纯粹克隆场景下并不需要,但也带来一些性能开销。如果仅仅为了克隆,同样的数据,通过DeepCloner这样的库往往能更快。举例来说,对于一个包含几个属性的小对象,AutoMapper 克隆可能是微秒级,而 DeepCloner 则能达到数百纳秒级 (GitHub - marcelltoth/ObjectCloner: Insanely fast and capable Deep Clone implementation for .NET based on Expression Trees)。

还有几点需要考虑:

适用场景总结

使用 AutoMapper 来深拷贝对象可能适合如下情况:

需要考虑的是AutoMapper未来的许可证变化。截至本文时(2025年),AutoMapper 仍可免费使用,但作者计划将其商业化 (About the AutoMapper License Change · Issue #22582 · abpframework/abp · GitHub)。这意味着将来新版可能需要购买授权用于商业项目。如果企业或项目对这一点敏感,那么将 AutoMapper 作为深拷贝方案就需要谨慎权衡(也许可以锁定在某个MIT版本,但长期不是办法)。相反,Newtonsoft.Json、Protobuf-net、DeepCloner等都会一直是开源友好许可证,不会有这种后顾之忧。

性能与适用性比较

经过对以上几种方案的分析,我们可以从安全性、性能、通用性等方面对它们进行简要比较:

下表汇总了各方案的特点:

深拷贝方案安全性性能通用性/限制开源许可证
Newtonsoft.Json✅ 无反序列化漏洞较慢 (GitHub - marcelltoth/ObjectCloner: Insanely fast and capable Deep Clone implementation for .NET based on Expression Trees)通用任意可序列化对象
不保留引用,同一对象会多份拷贝
MIT ([Ionic Newton Jsoft license - PlayFab
Protobuf-net✅ 安全,高效二进制格式需[ProtoContract]标记类型
不支持未标记的数据字段
Apache 2.0
表达式树/反射库✅ 安全,纯内存操作很快 (GitHub - marcelltoth/ObjectCloner: Insanely fast and capable Deep Clone implementation for .NET based on Expression Trees)通用任意对象,支持私有字段
保留对象图拓扑结构
MIT(多数库) (GitHub - force-net/DeepCloner: Fast object cloner for .NET)
AutoMapper✅ 安全,无外部输入中等需为类型配置映射
仅复制公共属性,可能不完整克隆
MIT(未来可能商业) (About the AutoMapper License Change · Issue #22582 · abpframework/abp · GitHub)

注:上述比较为一般情况,具体性能也取决于对象大小和复杂度。

开源许可证与第三方库说明

在选择深拷贝实现方案时,许可证是一个不可忽视的因素。根据要求,我们优先考虑 MIT、Apache 2.0 等友好许可证的库,避免具有商业限制或不兼容开源协议的方案。下面对文中涉及的库和技术的授权许可做一个汇总说明:

总之,在许可证方面,Newtonsoft.Json、Protobuf-net 和各类专用深拷贝库都满足“友好许可证”的要求,可以在商业项目中免费使用和分发。而 AutoMapper 未来的变化需要注意,但在现有版本下也有MIT授权可用。只要在使用前确认所选库的许可证(通常在官网或NuGet页面会注明),即可避免法律风险。

总结与建议

综合以上分析,针对 .NET Framework 4.6.2 环境的安全深拷贝需求,我们有多种可行方案,每种都有优劣与适用场景。最后做几点总结和推荐:

综上所述,推荐的安全深拷贝实现是在确保安全的前提下,根据具体需求灵活选型:对于一般场景,可采用 JSON 序列化克隆AutoMapper(MIT 版本)这种简便方法;对于高性能要求,采用 Protobuf-netDeepCloner 等专用库。 (GitHub - marcelltoth/ObjectCloner: Insanely fast and capable Deep Clone implementation for .NET based on Expression Trees) (Do not use BinaryFormatter as it is insecure and vulnerable )在实际项目中,也可以组合使用多种方法:比如默认用DeepCloner,而对少数特殊对象用自定义方法处理。关键是所有方法都应遵循安全最佳实践并满足项目的兼容性和许可证要求。

最后,深拷贝机制本身不能解决所有问题,仍需开发者根据业务语义判断哪些对象需要深拷贝,深拷贝后的对象如何使用。合理地选择和应用上述方案,将能在 .NET Framework 4.6.2 平台上高效且安全地实现深拷贝功能。

参考资料:



Previous Post
EF Core多租户架构实战:单库与分库实现全解析
Next Post
🔒HTTPS原理全解析:从握手到数据加密的技术细节