☰
自定义 XML 加密器(Custom Xml Encryptor)实战:为 ASP.NET Core Data Protection 密钥接入可插拔加密方案
2026/10/16 1:49:44 网站建设 项目流程
  • 示例工程

【免费下载链接】practical-aspnetcore

Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.

项目地址:https://gitcode.com/gh_mirrors/pr/practical-aspnetcore
点击查看免费下载

数据保护(Data Protection)是 ASP.NET Core 中用于加密 Cookie、防伪令牌(Anti-Forgery)等敏感数据的核心安全栈,其底层依赖一个“密钥环(Key Ring)”,而密钥环中的主密钥(Master Key)在落盘持久化时默认需要经过 XML 加密保护。practical-aspnetcore仓库中的 custom-encryptor 示例 完整演示了如何通过实现IXmlEncryptor/IXmlDecryptor接口,把自定义的密钥 XML 加密/解密逻辑无缝接入数据保护框架。读完本文,你将掌握自定义 XML 加密器的接口契约、注册机制(基于KeyManagementOptions与IConfigureOptions)、密钥落盘后的 XML 形态,并能在自己的 ASP.NET Core 应用中复刻这套扩展方案。

一、示例概览:为什么需要自定义 XML 加密器

在 ASP.NET Core 数据保护体系中,IDataProtectionProvider/IDataProtector负责对业务数据进行加解密,而密钥本身(masterKey)也需要被保护——否则攻击者只要拿到密钥文件,就能解密所有受保护的数据。框架默认提供了若干内置的密钥 XML 加密方案(如 Windows 上的 DPAPI、基于 X.509 证书的CertificateXmlEncryptor),但在跨平台、云原生、需要对接公司级 KMS 等场景下,开发者往往希望用自己的加密实现。

本示例正是为这种需求提供的最小可运行参考:它位于仓库的 dataprotection 专题目录 下,与其并列的还有 default-settings、azure-storage-blob-key-store、azure-keyvault-storage-blob-key-store、ef-key-store、redis-key-store 等示例。本示例聚焦“加密方式可插拔”这一点,通过两个自定义类 + 一个扩展方法,将自定义加密器挂载到数据保护配置中。

工程文件 CustomEncryptor.csproj 表明其目标框架为net10.0,并显式引用了数据保护核心包:

<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>true</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="10.0.11" /> </ItemGroup>

整个示例只依赖这一个 NuGet 包,所有自定义加密逻辑全部由项目源码实现,便于逐行理解。

二、核心接口契约:IXmlEncryptor 与 IXmlDecryptor

自定义加密方案要生效,必须实现数据保护框架定义的两个接口(位于Microsoft.AspNetCore.DataProtection.XmlEncryption命名空间):

  • IXmlEncryptor:接收明文密钥元素XElement plaintextElement,返回EncryptedXmlInfo。EncryptedXmlInfo是“加密结果 + 解密器类型”的封装,框架会把返回的 XML 元素持久化到密钥存储中,并记录解密时该用哪个IXmlDecryptor类型。
  • IXmlDecryptor:接收从密钥存储读回的加密元素,将其还原为原始明文密钥 XML 元素。

这一对称设计让密钥的“写路径”与“读路径”完全由同一个自定义实现控制,框架自身不关心加密算法细节,只负责调用与编排。下面分别看示例中的两个实现。

三、加密器实现:CustomXmlEncryptor

示例的加密器位于 Services/CustomXmlEncryptor.cs,完整代码如下:

public class CustomXmlEncryptor : IXmlEncryptor { private readonly ILogger _logger; public CustomXmlEncryptor(IServiceProvider services) { _logger = services.GetRequiredService<ILoggerFactory>().CreateLogger<CustomXmlEncryptor>(); } public EncryptedXmlInfo Encrypt(XElement plaintextElement) { if (plaintextElement == null) { throw new ArgumentNullException(nameof(plaintextElement)); } _logger.LogInformation("Not encrypting key"); var newElement = new XElement("unencryptedKey", new XComment(" This key is not encrypted. "), new XElement(plaintextElement)); var encryptedTextElement = new EncryptedXmlInfo(newElement, typeof(CustomXmlDecryptor)); return encryptedTextElement; } }

需要特别说明的是:本示例的“加密”是一个刻意简化的演示。从_logger.LogInformation("Not encrypting key")这行日志就能看出,它并没有真正对密钥做加密,而是把明文密钥元素包进一个名为unencryptedKey的外层元素,并附带一条注释This key is not encrypted.。这样做的好处是:

  1. 完整展示了IXmlEncryptor的调用契约(输入明文元素 → 输出EncryptedXmlInfo);
  2. 让读者可以直观地在生成的密钥 XML 文件中看到自定义加密器的输出形态;
  3. 便于验证“自定义解密器能否把包装还原回去”这一闭环。

在生产代码中,你只需要在Encrypt方法里用真正的加密算法(如 AES-GCM、公司 KMS 的加密 API)替换这段包装逻辑,框架对接方式完全一致。

值得注意的实现细节是构造函数通过IServiceProvider解析ILoggerFactory来创建日志器,这暗示了自定义加密器本身是由 DI 容器创建、并可使用容器内服务的——后续的注册扩展方法正是基于这一点编写的。

四、解密器实现:CustomXmlDecryptor

对应的解密器位于 Services/CustomXmlDecryptor.cs:

public class CustomXmlDecryptor : IXmlDecryptor { private readonly ILogger _logger; public CustomXmlDecryptor(IServiceProvider services) { _logger = services.GetRequiredService<ILoggerFactory>().CreateLogger<CustomXmlDecryptor>(); } public XElement Decrypt(XElement encryptedElement) { if (encryptedElement == null) { throw new ArgumentNullException(nameof(encryptedElement)); } return new XElement(encryptedElement.Elements().Single()); } }

解密逻辑与加密逻辑严格对称:加密时把明文包成unencryptedKey外层元素,解密时就用encryptedElement.Elements().Single()取出唯一的内层元素,外包一层XElement还原为明文密钥 XML。Single()在这里既保证了解密结果的确定性,也隐式校验了加密元素的结构完整性——如果 XML 结构与预期不符(内层元素数量不是 1),解密阶段会直接抛出异常。

提示:因为本示例并未真正加密,所以“解密”只是去掉包装层。如果你在Encrypt里使用了真实加密算法,Decrypt中就要对应地先做算法解密,再返回还原出的明文XElement。

五、桥接层:用 UseXmlEncryptor 扩展方法挂载到 KeyManagementOptions

有了加密器与解密器,下一步就是把加密器“告诉”数据保护框架。示例通过扩展方法完成这一注册,代码位于 Extensions/DependencyInjection.cs:

public static IDataProtectionBuilder UseXmlEncryptor( this IDataProtectionBuilder builder, Func<IServiceProvider, IXmlEncryptor> factory) { builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(serviceProvider => { var instance = factory(serviceProvider); return new ConfigureOptions<KeyManagementOptions>(options => { options.XmlEncryptor = instance; }); }); return builder; }

这段代码揭示了框架的扩展原理,可以分三层理解:

  1. IDataProtectionBuilder是链式入口:AddDataProtection()返回的就是它,所有数据保护定制(密钥存储、加密器、密钥生命周期等)都通过它继续链式调用。
  2. KeyManagementOptions.XmlEncryptor是框架的“插槽”:密钥管理器在持久化密钥前,会检查KeyManagementOptions.XmlEncryptor是否被设置,若已设置则调用其Encrypt方法。
  3. IConfigureOptions<T>是延迟配置的惯用手法:扩展方法向 DI 容器注册一个IConfigureOptions<KeyManagementOptions>,并接收Func<IServiceProvider, IXmlEncryptor>工厂。这样加密器实例在KeyManagementOptions被构建时才通过工厂创建,从而可以访问完整的服务提供程序(这正是构造函数能拿到ILoggerFactory的原因)。

工厂签名Func<IServiceProvider, IXmlEncryptor>也给了调用方最大灵活性:既可以直接s => new CustomXmlEncryptor(s)创建实例,也可以从s中解析其他服务来构造更复杂的加密器。

六、注册与密钥持久化:Startup 与最小托管两种写法

示例同时提供了两种托管风格的注册代码。

传统 Startup 写法(Startup.cs 的ConfigureServices):

public void ConfigureServices(IServiceCollection services) { var keysFolder = Path.Combine(Directory.GetCurrentDirectory(), "temp-keys"); services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(keysFolder)) .UseXmlEncryptor(s => new CustomXmlEncryptor(s)); services.AddRazorPages(); }

最小托管写法(Program.cs):

var builder = WebApplication.CreateBuilder(); var keysFolder = Path.Combine(Directory.GetCurrentDirectory(), "temp-keys"); builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(keysFolder)) .UseXmlEncryptor(s => new CustomXmlEncryptor(s)); builder.Services.AddRazorPages(); var app = builder.Build(); app.UseStaticFiles(); app.MapRazorPages(); app.Run();

两处配置都包含两个关键步骤,缺一不可:

  • PersistKeysToFileSystem(new DirectoryInfo(keysFolder)):指定密钥环持久化到temp-keys目录(相对当前工作目录),这是本示例选用的密钥存储方式。仓库的dataprotection专题还提供了其他存储方案(Azure Blob、EF Core、Redis),存储层与加密层相互独立,可以任意组合。
  • UseXmlEncryptor(s => new CustomXmlEncryptor(s)):把第一节实现的加密器接入KeyManagementOptions。

原文档特别强调:“Keys are stored on disk in a temp folder. Not advisable for production scenario.”即把密钥明文存放在本地临时目录仅用于演示,生产环境必须使用安全存储(如 Azure Key Vault + 托管身份)并启用真正的密钥加密,这一点将在后文再次展开。

七、运行示例:页面交互与数据加解密闭环

示例用 Razor Pages 构建了一个可交互的演示页,相关代码见 Pages/Index.cshtml 与 Pages/Index.cshtml.cs。

页面的核心逻辑在IndexModel中:构造函数通过IDataProtectionProvider.CreateProtector("DP.DefaultSettings.V1")创建一个带用途字符串的IDataProtector;提交表单后,OnPost分别调用Protect与Unprotect:

public void OnPost() { EncryptedString = _protector.Protect(StringToEncrypt); DeCryptedString = _protector.Unprotect(EncryptedString); }

页面最终把“加密后的字符串”和“解密还原后的字符串”同时展示给用户。整个数据保护链路因此形成完整闭环:

  1. 应用启动时,密钥管理器读取/创建密钥环,并将密钥通过CustomXmlEncryptor包装后写入temp-keys目录;
  2. 页面调用Protect加密用户输入,加密过程使用密钥环中的主密钥;
  3. 页面调用Unprotect解密,框架从密钥存储读回密钥 XML,通过EncryptedXmlInfo中记录的解密器类型实例化CustomXmlDecryptor,还原主密钥后完成解密。

运行方式(在示例目录下):

dotnet restore dotnet run

访问首页即可看到上文截图中的界面:输入任意字符串、点击 Submit,即可在页面上一并看到加密结果与解密结果。项目根目录的 global.json 声明了 SDK 版本要求(10.0.0,允许向前滚动),运行前请确保本机安装了匹配的 .NET SDK。

八、从持久化密钥文件看自定义加密器的“作品”

验证自定义加密器是否真正生效,最直接的办法是查看temp-keys目录下生成的密钥文件。仓库保留了一个实际生成的示例:temp-keys/key-7c2fc905-2a4b-4da4-b168-c0764b406eff.xml。其结构如下:

<key id="7c2fc905-2a4b-4da4-b168-c0764b406eff" version="1"> <creationDate>2022-04-27T08:47:08.0903902Z</creationDate> <activationDate>2022-04-27T08:47:08.0125044Z</activationDate> <expirationDate>2022-07-26T08:47:08.0125044Z</expirationDate> <descriptor deserializerType="...AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=6.0.0.0, ..."> <descriptor> <encryption algorithm="AES_256_CBC" /> <validation algorithm="HMACSHA256" /> <encryptedSecret decryptorType="CustomEncryptor.Services.CustomXmlDecryptor, CustomEncryptor, Version=1.0.0.0, ..."> <unencryptedKey xmlns=""> <!-- This key is not encrypted. --> <masterKey p6:requiresEncryption="true" xmlns:p6="..."> <!-- Warning: the key below is in an unencrypted form. --> <value>iAomnWjv9giI0XmBnaJ+1FBOin8j3x/X+At8wMix/GOsQrCbEO5Olo2an7LkqzTucdbpl3SIuQjW2RLQhEEJmA==</value> </masterKey> </unencryptedKey> </encryptedSecret> </descriptor> </descriptor> </key>

这份文件从源码层面印证了自定义加密器的完整工作链路:

  1. decryptorType指向自定义解密器:encryptedSecret元素的decryptorType属性值正是CustomEncryptor.Services.CustomXmlDecryptor, CustomEncryptor,说明框架在持久化时忠实记录了EncryptedXmlInfo中携带的解密器类型,读取密钥时据此创建解密实例。
  2. unencryptedKey元素与注释是加密器的直接产物:<unencryptedKey xmlns="">和<!-- This key is not encrypted. -->正是CustomXmlEncryptor.Encrypt中new XElement("unencryptedKey", new XComment(...))的输出,证明密钥确实经过了自定义加密器处理后落盘。
  3. masterKey的requiresEncryption="true"与“明文警告”注释:这是数据保护框架自身的元数据标记,意在提示该主密钥本应加密存储。由于本示例的加密器是“不加密”的演示实现,密钥以 Base64 明文形式躺在<value>中——这是刻意为之,便于读者对照理解;切勿照搬此配置用于生产。
  4. 密钥生命周期三时间戳:creationDate/activationDate/expirationDate分别表示密钥创建、启用与过期时间(示例中有效期约 90 天),过期后密钥管理器会自动滚动生成新密钥,这是 Data Protection 密钥自动轮换机制的体现。
  5. 算法声明:<encryption algorithm="AES_256_CBC" />与<validation algorithm="HMACSHA256" />声明了该密钥用于业务数据加解密时的认证加密算法组合(默认实现),与自定义的“密钥 XML 加密器”属于不同层级,二者并行不悖。

九、横向参照:同一专题下的其他密钥存储与加密方案

自定义 XML 加密器只是数据保护扩展的一个维度。仓库的 dataprotection 专题 README 将存储层与加密层做了系统性的示例覆盖,与本示例形成互补:

示例解决的问题关键 API
default-settings不配置任何东西时的默认行为services.AddDataProtection()
custom-encryptor(本示例)自定义密钥 XML 加密/解密IXmlEncryptor/IXmlDecryptor+UseXmlEncryptor
azure-storage-blob-key-store密钥环存到 Azure Blob,多实例共享PersistKeysToAzureBlobStorage
azure-keyvault-storage-blob-key-store用 Azure Key Vault 加密密钥 + Blob 存储ProtectKeysWithAzureKeyVault
ef-key-store密钥环存到关系数据库PersistKeysToDbContext
redis-key-store密钥环存到 RedisPersistKeysToStackExchangeRedis

这些示例彼此正交:存储层决定“密钥 XML 放哪里”,加密层决定“落盘前怎么保护”。本示例聚焦后者,让你能以最小代价理解扩展点的接线方式。

十、生产环境注意事项

结合原文档的警告与源码事实,在生产中使用自定义 XML 加密器时务必注意:

  1. 本示例的加密器是占位演示:它明确“不加密”密钥,仅验证扩展机制。生产环境的Encrypt必须调用真实加密算法(如 AES-256-GCM、或云厂商 KMS 的加密接口),并在Decrypt中对应还原。
  2. 密钥文件默认有效期约 90 天:从示例密钥文件的expirationDate可以看到默认轮换周期。在 Web 场(多实例)部署时,需确保所有实例共享同一密钥存储,否则新实例无法解密旧实例生成的数据。
  3. 密钥存储位置要安全:原文档明确表示temp-keys本地目录“不建议用于生产”。生产场景应使用受控的集中式存储(Azure Blob / 数据库 / Redis),并可结合 Azure Key Vault 等托管加密服务——这正是仓库中另外几个示例的用途。
  4. 改动加密器类型要谨慎:已持久化的密钥 XML 中decryptorType记录了解密器程序集限定名,若日后重命名类型或程序集,框架将无法实例化解密器,导致既有密钥不可用。生产变更应规划好密钥迁移/重新加密策略。
  • 示例工程

【免费下载链接】practical-aspnetcore

Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.

项目地址:https://gitcode.com/gh_mirrors/pr/practical-aspnetcore
点击查看免费下载

相关推荐

上一篇:小狼毫与Rime引擎:理解输入法核心架构
下一篇:Digital中的数字锁相环设计:时钟生成与同步技术

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询