很多人一开始和我一样,觉得只要不把C#源码交出去,客户拿到手的.dll和.exe就是安全的。直到前段时间我用反编译工具检查NuK某个版本的发布包,才发现这种想法错得离谱。NuK是我们团队维护的一个基于.NET 8的业务服务端项目,发布产物里包含授权计算、内部接口参数、敏感算法逻辑,结果用工具一打开,代码几乎和源码一样清楚。这篇文章就把我给NuK接入Obfuscar做代码混淆的完整过程写出来,包括配置文件怎么写、排除规则怎么定、怎么接进自动发布流程,以及我在实战中踩过的坑和教训。
1. 先从我的实际经历写起:NuK的发布包为什么需要再加一道防线
1.1 为什么以前觉得“看不到源码”就安全了
NuK早期是作为内部系统使用的,部署在客户内网,所以我一度认为安全压力不大。后来业务越做越深入,NuK开始以独立交付包的形式提供给合作方部署,这就意味着我们无法控制拿到程序集的人到底会怎么处理这些文件。交付包里除了配置文件,剩下的就是编译好的DLL。我当时的心理是:C#代码已经编译成IL了,普通使用者拿到DLL也看不懂,应该没问题。
这个想法听起来有道理,但对于.NET程序集来说,编译产生的IL携带了绝大多数原始语义信息。只要有一个ILSpy或者dnSpy,把一个DLL拖进去,就能得到可读程度很高的代码。更关键的是,类名、方法名、属性名、字符串常量全都保留了,阅读起来和看源码差距并不大。对NuK这种承载业务规则的服务来说,这等于把核心逻辑直接摊开给别人看。
1.2 用反编译工具打开程序集后看到的场景
我第一次拿ILSpy打开NuK的发布DLL时,先看到的是熟悉的命名空间和类名,比如NuK.Core.License、NuK.Core.PricingRule、NuK.Api.Controllers.OrderController,层次结构一目了然。继续往下点开一个和授权计算相关的类,里面有一个方法专门负责校验许可证签名,签名逻辑、绕过条件、内部使用的硬编码参数,在反编译视图里完整可见。
这不是危言耸听。我挑一段和NuK真实逻辑无关的简化示例来说明,比如这样一个类:
csharp复制namespace NuK.Core.License
{
internal sealed class LicenseValidator
{
private static readonly string _salt = "NuK-INTERNAL-ROOT";
internal static bool Verify(string fileContent)
{
var expected = $"nuk-{_salt}-{Calc(fileContent)}";
return expected == fileContent;
}
private static string Calc(string input)
{
// 业务hash计算
return input.Trim().ToUpperInvariant();
}
}
}
在反编译工具中,你看到的不是一团乱码,而是几乎原样的类名、方法名,甚至连_salt这个私有字段名都还在。攻击者不需要读懂整个业务,只要在搜索结果里定位License、Authorization、Key这些关键词,就能快速圈定关键代码区域。
1.3 这些风险值不值得动
我不是说所有项目都必须做代码混淆。如果只是一个纯内部服务,部署环境完全可控,那么做混淆的收益会低一些。但NuK的情况不同:交付物会被放到合作方的服务器上,合作方可能有自己的技术人员,也可能有第三方维保团队介入。程序集一旦脱离你的物理控制,反编译就是分分钟的事。
在排查了风险后,我当时确认了至少三类需要保护的内容:
- 授权与许可证逻辑。如果授权算法被逆向,攻击者可以伪造授权文件。
- 内部业务规则中的敏感计算逻辑。比如报价计算、任务调度策略、业务优先级规则。
- 硬编码的数据库连接字符串、内部接口地址、密钥等常量。
这些内容单靠“编译一次”根本保护不了,必须对最终发布的程序集做额外的弱化处理。于是我把目光放到了代码混淆工具上。也正是在这个背景下,我开始了对Obfuscar的调研和实践。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Obfuscar选型:保护成本和NuK的实际处境如何平衡
2.1 .NET生态里代码混淆有几条路可以走
提到.NET代码混淆,稍微了解过的人会先想到几个方向:商业混淆器、开源混淆器、以及“篡改检测+反调试”这类组合方案。
商业工具里比较常见的有 .NET Reactor、Dotfuscator、SmartAssembly 等。它们功能丰富,操作界面友好,有的还支持控制流混淆、字符串加密、反调试等花式功能,对安全性要求很高的商业产品来说确实省心。但价格不低,而且很多功能在NuK这种交付周期很紧的项目里,其实用不上多少。
开源方案里最常被讨论的是ConfuserEx。ConfuserEx的控制流混淆和字符串加密做得确实不错,社区名气也大。但ConfuserEx最大的问题是维护状态不稳定,项目更新停滞了很多年,对较新.NET版本的支持也就没那么顺。实际接入过程中,很容易遇到程序集处理失败、运行时崩溃这些烦心事。
Obfuscar是另一个老牌开源混淆器,用Mono.Cecil来处理程序集,支持命令行和MSBuild集成,历史比很多工具都长。它没有那么多花哨功能,核心就是重命名和部分字符串处理。我把它放在对比列表里时,刚开始也觉得“功能是不是太简单了”,但实际用下来发现,对于NuK这种以业务类库为核心的项目,它反而是试错成本最低的选择。
下表是我在选型时做过的粗略对比:
| 方案 | 混淆强度 | 社区维护 | 接入成本 | 对.NET 8的兼容性 | 许可证成本 |
|---|---|---|---|---|---|
| .NET Reactor | 强(控制流+加密) | 商业公司维护 | 中等 | 较好 | 高 |
| Dotfuscator | 较强 | 商业公司维护 | 中等 | 较好 | 高 |
| ConfuserEx | 强(控制流+加密) | 基本停滞 | 较高 | 一般 | 免费 |
| Obfuscar | 中等,偏重命名 | 社区活跃 | 低 | 较好 | 免费 |
2.2 NuK当时真正需要的是什么
如果只看“混淆强度”,Obfuscar不一定排在最前面,它缺少对IL控制流的深度混淆,所以反编译者仍然可以把方法体读出来。但NuK的核心诉求其实不是让反编译者“完全读不懂方法逻辑”,而是让他在拿到DLL后无法快速定位到关键类和方法。
这一点很重要。NuK的业务逻辑散落在很多类里,如果类名、方法名、字段名全部变成a、b、A_0这种无意义字符,即使反编译工具能把方法体还原出来,你也得花大量时间去猜哪段方法对应哪个业务功能。在没有符号信息、没有注释、没有原始命名的情况下,人工逆向成本会成倍增加。
Obfuscar做的正是这件事。它可以把命名空间、类名、方法名、字段名重写成短小的随机字符,还能隐藏部分私有API,并输出混淆后的程序集。配置方式也算简单,核心就是一份XML文件。对于一个不想在安全方面投入过多维护成本、又希望尽快看到效果的团队来说,Obfuscar是比较合适的切入点。
我在后来和同事讨论时也表达过这个观点:我们并不追求“绝对无法被逆向”,这是不现实的。我们需要的是把逆向成本提高到一个让大多数人不愿意继续投入的程度。从这个角度说,Obfuscar已经够用。
2.3 关于“坚不可摧”我一开始就有的清醒认识
项目标题里写“坚不可摧的安全防线”,但我必须说句实在话:没有任何静态代码混淆能带来绝对的不可破解性,因为最终程序集还是要被CLR加载执行,只要程序能运行,就存在被分析的可能。Obfuscar所能做的,是让程序集的可读性大幅下降,让攻击者无法通过简单的字符串搜索和类名定位直达要害。
所以我在设计NuK的保护方案时,并没有把Obfuscar当成唯一的安全手段,而是把它作为“纵深防御”的一层:代码混淆提高逆向难度,部署环境中再配合必要的访问控制、最小权限配置、密钥管理和日志审计。这几层加在一起,才算是相对完整的防线。
有了这个预期,再回头看Obfuscar的各种功能,就不会因为“它没有控制流混淆”而失望,反而能把它的重命名能力发挥到最大。
3. 把Obfuscar接进NuK:从发布目录到第一份混淆配置
3.1 先准备好可以被打包的程序集
整个流程的第一步,是先得到一份正常的发布产物。NuK项目我用的是dotnet publish,发布目录单独放在artifacts/publish底下,不要让Obfuscar直接在处理源项目的bin目录里动手,避免重复混淆或者把中间过程的文件搞乱。
我常用的发布命令大概是这样的:
bash复制dotnet publish NuK.Api/NuK.Api.csproj -c Release -o artifacts/publish
执行完以后,artifacts/publish目录下会有NuK.Api.dll、NuK.Core.dll,以及一堆依赖DLL和配置文件。这里要注意,Obfuscar需要处理的不是项目里的源码文件,而是发布出来的程序集文件,所以要确认发布目录里的文件确实是目标版本。
3.2 安装Obfuscar
Obfuscar的接入方式不少,推荐使用NuGet里的MSBuild包,也可以直接拿控制台工具跑。我给NuK搭的流程里,用的是“包引用+命令行执行”的组合方式,这样既方便在本地调试,也能在CI流水线里复用。
在NuK.Api工程里我加了一个包引用:
xml复制<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Obfuscar" Version="4.*" PrivateAssets="all" />
</ItemGroup>
</Project>
如果你使用的是.NET全局工具的方式,也可以参考下面这种安装方式:
bash复制dotnet tool install --global Obfuscar.GlobalTool
两种方式取其一就好。我个人更习惯通过包引用来锁定版本,因为构建服务器的环境更可控,不会出现全局工具版本漂移的问题。
3.3 写一份能跑通的最小配置
Obfuscar通过XML文件描述规则。我最初建了一个名为obfuscar.nuk.xml的配置文件,内容非常基础,主要是把输入输出路径和几个关键开关设好:
xml复制<?xml version="1.0" encoding="utf-8"?>
<Obfuscator>
<Var name="InPath" value="../artifacts/publish" />
<Var name="OutPath" value="../artifacts/obfuscated" />
<Var name="KeepPublicApi" value="true" />
<Var name="HidePrivateApi" value="true" />
<Var name="RenameProperties" value="true" />
<Var name="RenameEvents" value="true" />
<Var name="RenameFields" value="true" />
<Var name="UseUnicodeNames" value="false" />
<Module file="$(InPath)/NuK.Api.dll" />
<Module file="$(InPath)/NuK.Core.dll" />
</Obfuscator>
配置文件里最关键的是Module标签,它告诉Obfuscar要处理哪些程序集。InPath和OutPath一定要分开,我后面会专门说明原因。
执行混淆时,我直接调用包里带的可执行文件:
bash复制obfuscar.console.exe obfuscar.nuk.xml
如果一切正常,artifacts/obfuscated目录下会出现混淆后的DLL文件,并且命令行会提示处理完成。
3.4 为什么要坚持InPath和OutPath分开
不少第一次上手的人图省事,直接把OutPath写到和InPath相同,结果跑出来一堆同名文件覆盖,程序集列表混乱,有时还会把原始文件弄坏。Obfuscar在读取输入后,会把输出文件写到指定目录,如果输出目录和输入目录重合,工具还可能出现“尝试读取一个正在写入的文件”这类问题。
更合理的做法是把混淆后的产物放到独立目录,比如artifacts/obfuscated。这样原始发布目录保持干净,混淆目录只包含最后要交付的内容,回头检查时也方便对比。
3.5 第一次跑通的观察结果
第一轮混淆跑通后,我把混淆前后的DLL都拖到ILSpy里做了对比。最直观的变化是内部类和私有方法的名字全都变成了类似a、b、b_0这样的短名称。使用KeepPublicApi=true时,对外暴露的公开API名字保存了,这样如果NuK被其他程序集引用时,不会出现无法解析的方法签名。
但第一轮我也发现一个问题:仅保留公开API还不够,NuK内部有不少通过反射加载的类型和配置项,这些类一旦被重命名,反射逻辑就找不到目标了。这个问题开启了我下一步的深入研究。
4. 排除规则比混淆操作本身更重要:反射、契约与配置的生存策略
4.1 混淆这把刀会先砍到谁
Obfuscar做重命名的逻辑很简单:把程序集里的类型和方法改成一个新的名字,然后在同一个程序集内部同步更新所有引用。如果代码只是在一个程序集内部互相调用,混淆基本不会引起逻辑错误。但问题往往出在程序集边界之外,也就是那些不是靠编译期直接引用、而是靠字符串名字去查找类型的地方。
对NuK来说,最大的风险点有三个。
第一个是反射。比如代码里有Type.GetType("NuK.Core.SomeService")这样的调用,混淆后NuK.Core.SomeService可能已经变成了NuK.Core.a,运行时会直接抛TypeLoadException。
第二个是配置文件。.NET的配置体系经常通过类型名称来创建实例,如果配置节里写了类的完全限定名,而混淆后名字变了,配置加载就会失败。
第三个是外部契约。NuK如果对外提供API,客户端和服务端之间通过JSON交换数据,属性名一旦被重命名,客户端收到的JSON结构就和原先不一致,接口瞬间被破坏。
4.2 为NuK建立第一批排除清单
在理解了风险点之后,我就可以给Obfuscar配置排除规则了。排除规则的粒度可以到程序集、类型以及类型成员。我当时在配置文件中增加了一些类似下面的规则:
xml复制<Module file="$(InPath)/NuK.Api.dll">
<SkipType name="NuK.Api.Program" />
<SkipType name="NuK.Api.Startup" />
</Module>
<Module file="$(InPath)/NuK.Core.dll">
<SkipType name="NuK.Core.Configuration.*" />
<SkipType name="NuK.Core.Contracts.*" />
<SkipType name="NuK.Core.Serialization.*" />
</Module>
这样做的意图很明显:像Program和Startup这类由框架引导启动的类,名字一旦变化可能导致入口点都无法正确识别。而配置相关的类、契约DTO类、序列化辅助类更需要保持稳定,因为它们牵涉到反射和外部协议。
这里我特别提醒一点:配置文件里路径值可能会因为脚本运行位置不同而出现歧义。我建议在XML里用相对路径,并且始终从固定目录执行命令,或者用CI脚本里动态生成的绝对路径来替换路径占位符。
4.3 依赖注入与配置绑定的取舍
NuK用了ASP.NET Core的依赖注入,控制器、服务、仓储这些类型大多通过构造函数注入。进行代码混淆时,如果只重命名私有成员,构造函数注入基本不受影响,因为DI容器按类型解析,类型本身没被改名。但如果把KeepPublicApi=false并且把所有公共类型都重命名,那么Controller类的路由注册就可能出现问题。
我当时没有把NuK所有类型一股脑全混淆,而是走了折中方案:KeepPublicApi=true保底,保证所有公开类型名字不变,重点隐藏私有成员和内部实现。虽然这种模式的混淆强度不如全量重命名,但它和ASP.NET Core运行机制、反射加载机制的兼容性高很多,适合对稳定性要求高的业务系统。
如果你希望在NuK内部追求更强的混淆,可以单独针对某些不涉及外部引用、不参与反射的程序集开启KeepPublicApi=false,并通过排除列表把NuK.Api.Contracts、Startup、Program以及所有被序列化的Model类保护起来。这是一个需要反复验证的过程,千万不要上去就全盘重命名。
4.4 关于名字重命名时容易忽略的序列化问题
序列化是Obfuscar带来的隐形杀手。即使你使用KeepPublicApi=true,如果启用了RenameProperties=true,公共属性本身不会被重命名,因为属性属于公开API的一部分。但如果某个属性是内部属性,并且你把它当成JSON序列化字段使用,那混淆后前端可能收到完全不同的字段名。
为了避免这类问题,我最终把NuK中所有需要参与网络传输的DTO类型都加入了排除列表,或者在属性级别使用[DataContract]/[DataMember]来显式固定序列化名字,这样即使Obfuscar重命名了属性,序列化后的JSON键名仍然稳定。
在实际项目中,我建议做一个“必须保留名字”的清单,把它和代码仓库放在一起,每次调整配置时都能对照着检查。
5. 集成进自动发布流程:如何避免每次手工执行混淆工具
5.1 通过MSBuild目标自动触发混淆
NuK的发布动作不能停留在“本地手工执行”的层面,因为团队里多人参与,不同人手工操作很容易漏步骤。我的做法是新增一个专门负责发布的工程或脚本,在正常dotnet publish完成后自动调用Obfuscar。
如果用的是MSBuild方式,可以在发布项目中加入类似下面的Target:
xml复制<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishDir>artifacts/publish</PublishDir>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Obfuscar" Version="4.*" PrivateAssets="all" />
</ItemGroup>
<Target Name="RunObfuscarAfterPublish" AfterTargets="Publish">
<Exec Command=""$(ObfuscarConsole)" obfuscar.nuk.xml" />
</Target>
</Project>
当然,不同版本的MSBuild包可能对任务注入方式有差异,最稳妥的做法是先明确拿到Obfuscar控制台程序的路径,再用Exec命令去执行。我在CI脚本里用的命令简化后大概是这样的:
bash复制dotnet publish NuK.Api -c Release -o artifacts/publish
mono tools/Obfuscar.Console.exe obfuscar.nuk.xml
我选择这种方式是因为它足够透明,出错时排查路径很短,不会出现“明明构建成功但混淆没有跑”的黑盒情况。
5.2 千万不要对同一份程序集反复混淆
有一个非常容易踩的坑:发布流水线里既在Build阶段混淆了一次,又在Publish任务结束后再混淆一次。Obfuscar不是幂等工具,已经混淆过的程序集再次混淆时,可能会因为符号已经被改成短名字而出现“找不到类型”“符号已存在”之类的异常,或者生成一份更难维护但未必更安全的产物。
我建议把混淆动作固定在“发布产物生成之后、归档压缩之前”这一个节点上,并且在脚本里加一个判断:如果artifacts/obfuscated目录已经存在,就先清空再生成,避免旧文件残留。
5.3 保留原始版本和混淆版本的对照记录
Obfuscar在重命名时会生成映射信息,可惜很多项目没有把这份映射文件保存下来。一旦线上出现问题,你拿到的是混淆后的DLL,但PDB和源码都是原始命名,排查问题会非常痛苦。
我的做法是把混淆后的DLL连同符号文件、以及Obfuscar生成的映射文件一起归档到版本目录。这样如果线上日志里出现某个异常堆栈,即使类名已经被改写成a或b,我也可以通过映射关系反查回去,定位到真正的源码位置。
这一步在很多人眼里是额外工作,但做过一次线上排障之后你就知道它有多重要。
6. 用反编译工具验收:混淆效果不能只靠“跑起来没报错”来判断
6.1 混淆后的反编译结果对比
接入Obfuscar之后,我习惯用两个标准来验收成果:程序能不能正常跑,反编译之后还容不容易看懂。
以下是一个简化的前后对比,大家感受一下差别。混淆前,反编译出来的是这样:
csharp复制public sealed class LicenseValidator
{
private string _salt = "NuK-INTERNAL";
internal bool Validate(string content)
{
return BuildHash(content) == GetExpected(content);
}
}
混淆后,即使KeepPublicApi=true保留了公开类名,只要方法名和字段名被重命名,反编译结果看起来也会变成:
csharp复制public sealed class LicenseValidator
{
private string a = "...";
internal bool b(string c)
{
return d(c) == e(c);
}
}
如果进一步设置KeepPublicApi=false,连类名都会变成单字母,普通阅读者会瞬间失去跟进线索。
这里有一个需要提醒的点:Obfuscar对方法方法体本身的重排能力很有限,它不会像ConfuserEx那样把控制流搅乱。所以如果某个方法逻辑很短、很清晰,反编译者依然能看懂它在干什么。因此,不要把“识别逻辑”的希望完全寄托在混淆器上,更重要的还是要避免把高价值算法写得过于直白。
6.2 混淆后的行为验证清单
我在NuK上做的回归测试不是简单启动一下就完事,而是会跑一遍相对完整的功能用例,重点关注以下环节:
- 授权模块是否还能正常生成和校验许可证。
- API接口是否返回正常,JSON字段名是否和混淆前一致。
- 配置系统是否还能加载,依赖注入是否能够解析所有服务。
- 日志模块是否正常,异常堆栈中的类名是否可理解,能不能通过映射文件反查。
- 定时任务、后台队列等不会被请求直接触发的功能,是否也正常工作。
在这些环节里,定时任务和后台队列最容易漏测,因为它们不会在应用启动那一下暴露问题,往往要等到特定时间点才会触发。
6.3 如何测试项目实际功能的快速自检建议
如果你不想每次都手工测几十个用例,可以写一个简单的冒烟测试脚本,通过NuK的公开接口把核心链路走一遍。比如先调用登录接口,再调用一个需要授权才能访问的业务接口,确认返回的数据格式和混淆前一致。由于混淆主要影响类型名和成员名,只要程序能正常启动、依赖注入能解析、HTTP路由能匹配,大部分问题在启动阶段就能暴露。
6.4 一个务实的结论:混淆让逆向成本提高多少
在我对NuK完成混淆后,我请团队里另一位同事尝试从混淆后的DLL里找到授权模块的核心逻辑。他花了大半个小时,第一反应是“到处都是a和b,很难判断从哪里开始”,最后是靠搜索硬编码字符串才勉强定位到一个相关方法。
这说明Obfuscar的重命名确实能显著提高人工逆向的门槛。但这并不等于“坚不可摧”。如果攻击者足够坚定,拥有很强的IL分析能力,配合调试工具动态跟踪,他最终依然可以还原出逻辑。所以我在项目里始终强调:代码混淆只是防线的一环,不能替代密钥管理、服务端校验、网络传输加密这些更底层的工作。
7. 我在NuK实践中踩过的坑和最终的落地经验
7.1 强名称程序集在混淆后会签名失效
NuK的早期版本给程序集加了强名称签名。第一次跑完Obfuscar后,我发现混淆后的DLL无法加载,提示强名称签名无效。原因是Obfuscar修改了程序集内容,原来的签名已经失效。
解决办法有两种。一种是在混淆前移除强名称签名,混淆后再补签一次,但这个过程会让构建流程复杂不少。另一种是干脆不再对内部程序集做强名称签名,或者只在最后一层做延迟签名。考虑到NuK并不是需要被其他外部程序集强引用发布的公共库,我最终选择了去掉强名称约束,并对发布包做一层完整性哈希校验。
如果你的场景必须保留强名称,请确保在构建脚本里考虑了混淆后的重签名步骤,否则上线时大概率会踩这个坑。
7.2 静态字符串依然是信息泄露点
Obfuscar可以帮你隐藏一部分字符串,但对于业务代码里硬编码的数据库连接串、外部接口的访问密钥,隐藏效果依然有限。我在检查混淆后的DLL时,发现有些明文API地址还是可以通过字符串搜索直接找到。
NuK的情况稍微特殊一点,因为它需要读取配置文件里的敏感信息,而我不能让DLL里完全不含任何线索。我的处理方式是把真正高价值的密钥迁移到环境变量或独立的密钥管理服务中,程序集里只存占位符或启动时动态获取。这样即使有人反编译DLL,也只能看到取密钥的代码,看不到密钥本身。
7.3 排除名单不要只写在本地,更要跟着代码库走
Obfuscar的配置文件应该纳入版本控制,并且每次修改之后都要在Code Review里被检查。因为排除名单一旦漏了某个关键的DTO类型,线上接口的兼容性就可能出问题;而排除名单如果过于宽松,混淆效果又会大打折扣。
我会在提交信息里写清楚这次改动了哪些排除类型,为什么要排除,让团队成员能完整理解变更背景。基于NuK这种多模块项目的经验,那些需要被排除的类型往往不是项目初始就能完整列出来的,而是在反射报错、序列化结果不一致、接口异常等实战反馈中逐步补全的。
7.4 从一次线上故障看混淆后如何保留排查能力
有一回NuK在测试环境出了一个偶发错误,线上日志里的异常堆栈显示方法名是NuK.Core.b.a,同事完全不认识这是哪个方法。我通过归档的映射文件定位后,才发现它对应的是报价计算模块里的一个内部方法,问题很快得到修复。
这次经历让我更加确定:混淆之后的产物,一定要把映射文件作为构建产物的一部分加以保存。Obfuscar的映射文件虽然不像PDB那样能直接定位到行号,但它能告诉你混淆前的类型和方法名,足以让开发人员快速找到源码位置。
7.5 我个人最终建议的NuK保护配置方向
经过反复测试后,如果让我给NuK推荐一套偏向稳定且有效的配置方向,大概是下面这样:
- 对外提供API的项目里,
KeepPublicApi不要轻易设为false。 - 真正承载核心算法的独立类库,可以考虑单独开启更强混淆,但要仔细排除所有被反射或序列化引用的类型。
- 不要在一个配置里试图覆盖所有程序集,分模块配置更清晰。
- 每次发布后固定保存一份“混淆映射+
