- 示例工程
【免费下载链接】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.
数据保护(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.。这样做的好处是:
- 完整展示了
IXmlEncryptor的调用契约(输入明文元素 → 输出EncryptedXmlInfo); - 让读者可以直观地在生成的密钥 XML 文件中看到自定义加密器的输出形态;
- 便于验证“自定义解密器能否把包装还原回去”这一闭环。
在生产代码中,你只需要在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; }这段代码揭示了框架的扩展原理,可以分三层理解:
IDataProtectionBuilder是链式入口:AddDataProtection()返回的就是它,所有数据保护定制(密钥存储、加密器、密钥生命周期等)都通过它继续链式调用。KeyManagementOptions.XmlEncryptor是框架的“插槽”:密钥管理器在持久化密钥前,会检查KeyManagementOptions.XmlEncryptor是否被设置,若已设置则调用其Encrypt方法。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); }页面最终把“加密后的字符串”和“解密还原后的字符串”同时展示给用户。整个数据保护链路因此形成完整闭环:
- 应用启动时,密钥管理器读取/创建密钥环,并将密钥通过
CustomXmlEncryptor包装后写入temp-keys目录; - 页面调用
Protect加密用户输入,加密过程使用密钥环中的主密钥; - 页面调用
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>这份文件从源码层面印证了自定义加密器的完整工作链路:
decryptorType指向自定义解密器:encryptedSecret元素的decryptorType属性值正是CustomEncryptor.Services.CustomXmlDecryptor, CustomEncryptor,说明框架在持久化时忠实记录了EncryptedXmlInfo中携带的解密器类型,读取密钥时据此创建解密实例。unencryptedKey元素与注释是加密器的直接产物:<unencryptedKey xmlns="">和<!-- This key is not encrypted. -->正是CustomXmlEncryptor.Encrypt中new XElement("unencryptedKey", new XComment(...))的输出,证明密钥确实经过了自定义加密器处理后落盘。masterKey的requiresEncryption="true"与“明文警告”注释:这是数据保护框架自身的元数据标记,意在提示该主密钥本应加密存储。由于本示例的加密器是“不加密”的演示实现,密钥以 Base64 明文形式躺在<value>中——这是刻意为之,便于读者对照理解;切勿照搬此配置用于生产。- 密钥生命周期三时间戳:
creationDate/activationDate/expirationDate分别表示密钥创建、启用与过期时间(示例中有效期约 90 天),过期后密钥管理器会自动滚动生成新密钥,这是 Data Protection 密钥自动轮换机制的体现。 - 算法声明:
<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 | 密钥环存到 Redis | PersistKeysToStackExchangeRedis |
这些示例彼此正交:存储层决定“密钥 XML 放哪里”,加密层决定“落盘前怎么保护”。本示例聚焦后者,让你能以最小代价理解扩展点的接线方式。
十、生产环境注意事项
结合原文档的警告与源码事实,在生产中使用自定义 XML 加密器时务必注意:
- 本示例的加密器是占位演示:它明确“不加密”密钥,仅验证扩展机制。生产环境的
Encrypt必须调用真实加密算法(如 AES-256-GCM、或云厂商 KMS 的加密接口),并在Decrypt中对应还原。 - 密钥文件默认有效期约 90 天:从示例密钥文件的
expirationDate可以看到默认轮换周期。在 Web 场(多实例)部署时,需确保所有实例共享同一密钥存储,否则新实例无法解密旧实例生成的数据。 - 密钥存储位置要安全:原文档明确表示
temp-keys本地目录“不建议用于生产”。生产场景应使用受控的集中式存储(Azure Blob / 数据库 / Redis),并可结合 Azure Key Vault 等托管加密服务——这正是仓库中另外几个示例的用途。 - 改动加密器类型要谨慎:已持久化的密钥 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.
相关推荐
ASP.NET Core Data Protection 密钥存储与加密实战:默认设置、Azure、EF Core、Redis 与自定义 XML 加密器全解析
ASP.NET Core Data Protection 密钥存储与加密实战:默认设置、Azure、EF Core、Redis 与自定义 XML 加密器全解析
示例工程ExoPlayer HLS加密密钥获取:自定义密钥服务器实现
ExoPlayer HLS加密密钥获取:自定义密钥服务器实现 1. HLS加密播放痛点与解决方案 你是否在集成HLS加密流时遇到过这些问题:密钥服务器认证逻辑复
音视频移动开发fuel-core密钥管理:加密密钥的安全存储
fuel core密钥管理:加密密钥的安全存储 概述 在区块链系统中,密钥管理是安全性的核心。fuel core作为Fuel区块链的客户端实现,提供了完善的密钥
区块链后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考