65.9K
CodeProject 正在变化。 阅读更多。
Home

Fededim.Extensions.Configuration.Protected.DataProtectionAPI:ASP.NET Configuration 和 Data Protection API 之间的终极集成

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2023 年 12 月 16 日

CPOL

29分钟阅读

viewsIcon

45611

Fededim.Extensions.Configuration.Protected 是一个改进的 ConfigurationBuilder,它允许对存储在任何可能的 ConfigurationSource 中的配置值进行部分或完全加密,并完全集成到 ASP.NET Core 架构中。

引言

大约一个月前,我发布了这篇关于改进 JSON 文件的 ConfigurationSourceConfigurationProvider 的文章ProtectedJson,它允许使用 Data Protection API 对配置值进行部分或完全加密。收到了一些评论,其中一个不那么“有意义”的问题是我的包是否也支持环境变量。

尽管我们可以疑惑为什么会有人想要加密/解密环境变量,但这个问题让我有了一个顿悟:我为 JSON 文件所做的工作是否也可以扩展到其他配置源?经过下班后的小型概念验证项目,答案是肯定的,经过一些返工,我发布了一个新包Fededim.Extensions.Configuration.Protected,它最重要的是 ProtectedJson 的改进和扩展,以支持加密/解密存储在**任何**配置源中的配置值。

主要特点

  • 部分或完全加密配置值
  • 适用于任何现有和(希望)未来的 ConfigurationSourceConfigurationProvider(已成功使用框架内置提供程序进行测试,例如 CommandLineEnvironmentVariablesJsonXmlInMemoryCollection
  • 加密值在内存中透明解密,几乎不需要任何额外的代码行
  • 支持全局配置和任何 ConfigurationSource 的自定义覆盖
  • 支持几乎所有 .NET 框架(net6.0、netstandard2.0 和 net462)
  • 易于插入任何现有的 .NET / .NET Core 项目
  • 如果底层 IConfigurationProvider 支持,则支持在配置重新加载时自动重新解密
  • 支持每个配置值派生子密钥加密(称为“子用途”)
  • 支持可插拔加密/解密,使用实现标准接口 IProtectProvider 的不同提供程序(自 1.0.12 版本以来,请记住实现安全且强大的加密/解密提供程序需要深入的安全知识!)。

背景

ASP.NET Configuration 是 .NET Core 存储应用程序配置数据的标准方式,通过各种配置源(通常是 JSON 文件,但也包括环境变量、XML 文件、内存中字典、命令行参数或您希望实现的任何自定义提供程序)中的分层键值对。虽然 .NET Framework 使用单个源(通常是 XML 文件,它本质上更冗长),但 .NET Core 可以使用多个有序的配置源,这些源会“合并”,从而允许用后续配置源中存在的相同键值覆盖配置源中键值的概念。这很有用,因为在软件开发中,通常有多个环境(开发、集成、预生产和生产),每个环境都有自己的自定义设置(例如,API 端点、数据库连接字符串、不同的配置变量等)。在 .NET Core 中,这种管理非常简单,事实上,您通常有两个 JSON 文件

  • appsettings.json:包含所有环境通用的配置参数
  • appsettings.<environment name>.json:包含特定于特定环境的配置参数

ASP.NET Core 应用程序通常配置并启动一个 host。主机负责应用程序启动、配置依赖注入和后台服务、配置日志记录、生命周期管理以及显然配置应用程序配置。这主要通过两种方式完成

  • 隐式地,通过使用框架提供的方法之一,例如 WebApplication.CreateBuilderHost.CreateDefaultBuilder(通常在 Program.cs 源文件中调用),它实质上执行以下操作
    • 读取和解析命令行参数
    • 分别从 ASPNETCORE_ENVIRONMENTDOTNET_ENVIRONMENT 环境变量中检索环境名称(在操作系统变量中设置或直接在命令行中通过 --environment 参数传递)。
    • 读取并解析两个名为 appsettings.jsonappsettings.<environment name>.json 的 JSON 配置文件。
    • 读取并解析环境变量。
    • 调用 ConfigureAppConfiguration 的委托 Action<Microsoft.Extensions.Hosting.HostBuilderContext, Microsoft.Extensions.Configuration.IConfigurationBuilder>,您可以在其中通过 IConfigurationBuilder 参数配置应用程序配置
  • 显式地通过实例化 ConfigurationBuilder 类并使用其中一个提供的扩展方法
    • AddCommandLine:请求解析命令行参数(通过 ---/
    • AddJsonFile:请求解析 JSON 文件,指定它是否是强制的或可选的,以及它在文件系统上更改时是否应该自动重新加载。
    • AddXmlFile:请求解析 XML 文件,指定它是否是强制的或可选的,以及它在文件系统上更改时是否应该自动重新加载。
    • AddEnvironmentVariables:请求解析环境变量
    • 等等。

本质上,每个 Add<xxxx> 扩展方法都添加了一个 ConfigurationSource 来指定键值对的来源(命令行、Json 文件、环境变量等)和一个相关的 ConfigurationProvider,用于将数据从源加载和解析到 IConfigurationRoot 接口的 Providers 列表中,该接口是 ConfigurationBuilder 类上 Build 方法的结果,如下图所示。

(在 configuration.Providers 中,有六个来源:CommandLineConfigurationProvider,两个用于 appsettings.jsonappsettings.<environment name>.jsonJsonConfigurationProvider,一个用于 XML 文件 appsettings.xmlXMLConfigurationProvider,一个用于提供的字典的 MemoryConfigurationProvider,最后是用于环境变量的 EnvironmentVariableConfigurationProvider)。

 

正如我前面所写,调用 Add<xxxx> 扩展方法的顺序很重要,因为当 IConfigurationRoot 类检索键值时,它使用 GetConfiguration 方法,该方法以**反向**顺序循环 Providers 列表,尝试返回包含查询键的第一个,从而模拟所有配置源的“合并”(LIFO 顺序,后进先出)。

 

Using the Code

您可以在我的 Github 仓库中找到所有源代码,代码基于 .NET 6.0 和 Visual Studio 2022。解决方案文件中包含五个项目,两个是关于旧的 ProtectedJson,三个是关于这个新的 Protected 包,我们来谈谈最后四个

  • Fededim.Extensions.Configuration.Protected:本质上,此包实现了与 Microsoft ASP.NET Core Configuration API 集成的核心逻辑,将所有加密/解密代码委托给实现 IProtectProvider 接口(处理加密/解密的核心逻辑)和 IProtectProviderConfigurationData 接口(一个用于指定配置选项并将提供程序插入到 ProtectedConfigurationBuilder 的抽象类)的可插拔外部提供程序,ProtectedConfigurationBuilder 是用于解密存储在任何配置源中的任何配置值的启用类。此包实现的其他类型包括 ProtectedConfigurationProvider(实际负责解密配置值的主要类)、包含 IConfigurationBuilder 接口的扩展方法(WithProtectedConfigurationOptions 用于指定仅适用于特定 ConfigurationSource 的特定配置)的 ConfigurationBuilderExtensions,以及 ProtectFileOptions/ProtectFileProcessorsXmlProtectFileProcessorJsonProtectFileProcessorJsonWithCommentsProtectFileProcessorRawProtectFileProcessor)用于读取、解码、加密和重新编码源文件。此包已发布到 NuGet.Org

    随附的包 **Fededim.Extensions.Configuration.Protected.DataProtectionAPI** 提供了一个基于 **Microsoft Data Protection API** 的标准提供程序,如果您不打算开发另一个加密/解密提供程序,则只需在项目中引用此包(**再次强调,实现安全可靠的加密/解密提供程序需要深入的安全知识!**)。
     
  • Fededim.Extensions.Configuration.Protected.DataProtectionAPI:这是标准的 Microsoft Data Protection API 加密/解密提供程序,它分别使用其类 DataProtectionAPIProtectProviderDataProtectionAPIProtectConfigurationData 实现了接口 IProtectProvider 和抽象类 IProtectProviderConfigurationData此包也已发布到 NuGet.Org。
     
  • Fededim.Extensions.Configuration.Protected.DataProtectionAPITest:这是一个 xUnit 测试项目,它彻底测试了上述两个包,以提高可靠性和代码质量。它为 MS .NET 提供的所有 ConfigurationSources(一个 JSON 文件、一个 XML 文件、环境变量、一个内存字典和命令行参数)创建了包含 2*固定键集(10000)的示例数据,一个是用随机数据类型和值表示的纯文本,另一个是具有相同值但已加密的。然后它使用 ProtectedConfigurationBuilder 加载示例数据以进行解密,并测试所有纯文本值是否与已解密的值相同。
     
  • Fededim.Extensions.Configuration.Protected.ConsoleTest:这是一个控制台应用程序,它通过读取和解析六个加密的定制配置源,并将其转换为名为 AppSettings 的强类型类,来展示如何使用 ProtectedConfigurationBuilder。解密过程完美无瑕且自动进行,几乎不需要任何代码行,让我们看看如何实现。

要使用自动解密功能,您只需将调用 new ConfigurationBuilder() 替换为对 new ProtectedConfigurationBuilder() 的调用,并向其传递可插拔加密/解密提供程序的配置。完成此操作后,您可以通过使用标准方法(如 AddCommandLineAddJsonFileAddXmlFileAddInMemoryCollectionAddEnvironmentVariables,甚至未来的配置源)添加任何现有配置源,因为只要 Microsoft.Extensions.Configuration.ConfigurationProvider 的 GetChildKeys 实现不变,此包就应该支持所有这些源(请继续阅读下文以了解原因)。ProtectedConfigurationBuilder 的构造函数只接受一个参数,即派生自 IProtectProviderConfigurationData 的配置类,这是一个抽象类(由一个可用提供程序实现),用于指定配置选项并将提供程序插入到 ProtectedConfigurationBuilder 中。每个提供程序都必须指定四个常见的基本参数

  • ProtectedRegex:它是一个正则表达式,用于指定包含要解密的加密数据的标记;它必须定义一个名为 protectedData 的命名组(以及可选的两个名为 subPurposePatternsubPurpose 的附加组,用于指定每个配置值的子键)。如果为 null,则此参数采用默认值
    public const String DefaultProtectedRegexString = "Protected(?<subPurposePattern>(:{(?<subPurpose>[^:}]+)})?):{(?<protectedData>.*?)}";
    

    上述正则表达式本质上以惰性方式(因此它可以检索值内的所有出现)搜索与模式 'Protected:{<subPurpose>}:{<encrypted data>}' 匹配的任何 string,并提取 <encrypted data> 子字符串,将其存储在一个名为 protectedData 的组中。还有一个**可选**部分,称为 <subPurposePattern>(由 :{<subPurpose>} 组成),它允许指定每个配置值派生的子密钥(称为“子用途”,最初借鉴自 Data Protection API)。如果您不喜欢这种标记化,您可以用任何您喜欢的其他标记化替换它,方法是创建一个正则表达式,其限制是它必须将 <encrypted data> 子字符串提取到一个名为 protectedData 的组中,并将 <subPurposePattern><subPurpose> 子字符串提取到分别名为 subPurposePatternsubPurpose 的两个组中。

  • ProtectRegex:它是一个正则表达式,用于指定包含要加密数据的标记;同样,它必须定义一个名为 protectData 的命名组(以及可选的两个名为 subPurposePatternsubPurpose 的附加组,用于指定每个配置值的子密钥)。如果为 null,则此参数采用默认值(例如 Protect:{<subPurpose>}:{<data to be encrypted>}

    public const String DefaultProtectRegexString = "Protect(?<subPurposePattern>(:{(?<subPurpose>[^:}]+)})?):{(?<protectData>.*?)}";
    
  • DefaultProtectedReplaceString: 它是一个字符串表达式,用于将纯文本标记转换为加密标记(例如,从 Protect:{<subPurpose>}:{<data to be encrypted>} 转换为 Protected:{<subPurpose>}:{<encrypted data>})。它包含两个占位符 ${subPurposePattern}${protectedData},它们分别替换为 subPurposePattern(如果存在)和加密数据。如果为 null,此参数将采用默认值

public const String DefaultProtectedReplaceString = "Protected${subPurposePattern}:{${protectedData}}";
  • IProtectProvider:这是一个标准接口,必须由可插拔提供程序实现,以提供加密/解密服务。

默认情况下,随附的包 **Fededim.Extensions.Configuration.Protected.DataProtectionAPI** 提供了一个基于 Microsoft Data Protection API 的标准加密/解密提供程序。其实现 IProtectProviderConfigurationData 的配置类名为 **DataProtectionAPIProtectConfigurationData**,其构造函数除了上述三个通用的 ProtectedRegexProtectRegexDefaultProtectedReplaceString 参数外,还接受以下附加固有参数

  • dataProtectionServiceProvider:这是一个 IServiceProvider 接口,用于实例化 Data Protection API 的 IDataProtectionProvider 以解密数据。此参数与下一个参数互斥。
  • dataProtectionConfigureAction:这是一个 Action<IDataProtectionBuilder>,用于在标准 .NET Core 中配置 Data Protection API。同样,此参数与上一个参数互斥。
  • purposeString:用于显式指定用于加密的目的字符串。Data Protection API 支持多个加密密钥,这些密钥派生自配置的主密钥,并与传递给 CreateProtector API 的一个或多个 purpose 字符串严格相关,更多信息请阅读此处此处
  • keyNumber:此参数是 purposeString 的替代方案,为了方便起见 (我们更习惯于考虑不同的密钥编号而不是不同的用途),它用于指定一个用于加密的密钥索引(默认值为 1),在底层,提供的索引用于构造 purpose 字符串 $"Key{keyNumber}"(请参阅 ProtectedConfigurationBuilder.ProtectedConfigurationBuilderKeyNumberPurpose 方法)

dataProtectionServiceProviderdataProtectionConfigureAction 参数有点缺点,因为它们表示对另一个依赖注入的重新配置,用于实例化解密数据所需的 IDataProtectionProvider

事实上,在标准的 .NET Core 应用程序中,通常在读取和解析配置文件之后配置依赖注入(因此所有配置源和提供程序都不使用 DI),但在这种情况下,我不得不这样做,因为访问 Data Protection API 的唯一方法是通过 DI。此外,在配置依赖注入时,解析的配置通常通过使用 services.Configure<<强类型设置类>>(configuration) 绑定到强类型类,所以这是一个狗追尾巴(要解密配置需要 DI,要配置 DI,需要解析配置才能将其绑定到强类型类)。目前我能想到的唯一解决方案是重新配置一个仅用于 Data Protection API 的第二个 DI IServiceProvider,并在 ProtectedConfigurationProvider 中使用它。要配置第二个 DI IServiceProvider,您有两种选择

  • 您自己创建它(通过实例化 ServiceCollection 并对其调用 AddDataProtection
  • 您让 ProtectedConfigurationProvider 通过传递 dataProtectionConfigureAction 参数来创建它。在这种情况下,为了避免重复代码,Data Protection API 的配置可以在一个名为 ConfigureDataProtection 的公共 private 方法中执行,例如
private static void ConfigureDataProtection(IDataProtectionBuilder builder)
{
    builder.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
    {
        EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
        ValidationAlgorithm = ValidationAlgorithm.HMACSHA256,

    }).SetDefaultKeyLifetime(TimeSpan.FromDays(365*15)).PersistKeysToFileSystem
                                              (new DirectoryInfo("..\\..\\Keys"));
}

在这里,我选择使用 AES 256 对称加密,并以 HMAC SHA256 作为数字签名函数。此外,我要求将主加密密钥元数据(密钥、IV、哈希算法等)存储在控制台应用程序的 Keys 文件夹中的 XML 文件中(默认情况下,密钥根据 MS 密钥管理文档存储在特定位置,请注意所有这些 API 默认由 Data Protection API 提供)。

因此,当您第一次启动应用程序时,Data Protection API 会自动创建主加密密钥并将其存储在 Keys 文件夹中,在后续运行中,它会从该 XML 文件加载密钥。然而,从安全性角度来看,这种配置并非最佳方法,因为密钥以纯文本形式存储,如果您想加密静态主密钥,可以使用 ProtectKeysWithDpapi 扩展方法(仅适用于 Windows,在这种情况下,它将使用 Windows DPAPI 加密)或 ProtectKeysWithCertificate 使用安装在计算机上的证书对其进行加密。请注意,尽管您可以在 Keys 文件夹中使用不同的加密密钥,但只有一个主密钥,所有加密密钥都使用从 keyNumber 参数或 purposeString 参数(在 ProtectedConfigurationBuilderWithProtectedConfigurationOptions 扩展方法中指定)生成的 purpose 字符串派生。

在控制台应用程序中,我按以下顺序添加了六个配置源,以举例说明 ASP.NET Core Configuration 的合并功能以及加密值的使用

  • AddCommandLine:添加命令行参数。
  • AddJsonFile:添加**两个 json** 文件appsetting.jsonappsettings.development.json,第二个文件的 reloadOnChange 标志设置为 true,以允许在文件系统上更改时重新加载 json 文件。

    如果您查看 appsetting.json 的 **ConnectionStrings 部分**,有三个键

    • PlainTextConnectionString:顾名思义,它包含一个纯文本连接字符串
    • PartiallyEncryptedConnectionString:顾名思义,它包含纯文本和多个 Protect:{<data to encrypt>} 标记的混合。每次运行时,这些标记在调用扩展方法 IProtectProviderConfigurationData.ProtectFiles 后,都会自动加密并替换为 Protected:{<encrypted data>} 标记。
    • FullyEncryptedConnectionString:顾名思义,它包含一个单独的 Protect:{<data to encrypt>} 标记,该标记跨越整个连接字符串,并在第一次运行后完全加密。

    如果您查看 appsetting.development.json 的 **Nullable 部分**,您可以找到一些**有趣的键**

    • Int, DateTime, Double, Bool:这些键分别包含一个整数、一个日期时间、一个双精度浮点数和一个布尔值,但它们都以 string 形式存储,使用单个 Protect:{<data to encrypt>} 标签。嘿,等等,这怎么可能?

      嗯,主要是,所有的 ConfigurationProviders 最初都会在它们的 Load 方法中将任何 ConfigurationSource 转换为 Dictionary<String,String>(请参阅框架 ConfigurationProvider 基抽象类的 Data 属性Load 方法还将所有到键的层次路径扁平化为一个由冒号分隔的 string,例如 Nullable->Int 变成 Nullable:Int)。稍后,这个字典才会被转换并绑定到一个强类型类。

      ProtectedConfigurationProvider 的解密过程发生在中间,因此对用户是透明的,而且适用于任何简单的变量类型(DateTimebool 等)。目前,不支持对整个数组进行完全加密,但您可以通过将数组转换为字符串数组来加密单个元素(请查看 DoubleArray 键,还要注意在此键中示例了每个配置值子键的使用,事实上数组包含两次相同的 3.14 值 ["Protect:{3.14}""Protect:{%customSubPurpose%}:{3.14}"] 但它使用两个不同的键和不同的盐进行加密)。

  • AddXmlFile:添加 XML 文件 appsettings.xml
  • AddInMemoryCollection:添加内存中字典
  • AddEnvironmentVariables:添加环境变量

Protected.ConsoleTest 控制台应用程序的主要代码是

public static void Main(String[] args)
{
    args = new String[] { "--EncryptedCommandLinePassword", "Protect:{secretArgPassword!\\*+?|{[()^$.#}", "--PlainTextCommandLinePassword", "secretArgPassword!\\*+?|{[()^$.#" };

    // define the DI services: setup Data Protection API
    var servicesDataProtection = new ServiceCollection();
    ConfigureDataProtection(servicesDataProtection.AddDataProtection());
    var serviceProviderDataProtection = servicesDataProtection.BuildServiceProvider();


    // creates all the DataProtectionAPIProtectConfigurationData classes specifying three different provider configurations

    // standard configuration using key number purpose
    var standardProtectConfigurationData = new DataProtectionAPIProtectConfigurationData(serviceProviderDataProtection);

    // standard configuration using key number purpose overridden with a custom tokenization
    var otherProtectedTokenizationProtectConfigurationData = new DataProtectionAPIProtectConfigurationData(serviceProviderDataProtection,2, protectRegexString: "OtherProtect(?<subPurposePattern>(:{(?<subPurpose>[^:}]+)})?):{(?<protectData>.+?)}", protectedRegexString: "OtherProtected(?<subPurposePattern>(:{(?<subPurpose>[^:}]+)})?):{(?<protectedData>.+?)}", protectedReplaceString: "OtherProtected${subPurposePattern}:{${protectedData}}");

    // standard configuration using string purpose
    var magicPurposeStringProtectConfigurationData = new DataProtectionAPIProtectConfigurationData(serviceProviderDataProtection, "MagicPurpose"); 



    // activates JsonWithCommentsProtectFileProcessor
    ConfigurationBuilderExtensions.UseJsonWithCommentsProtectFileOption();

    // define in-memory configuration key-value pairs to be encrypted
    var memoryConfiguration = new Dictionary<String, String>
    {
        ["EncryptedInMemorySecretKey"] = "Protect:{InMemory MyKey Value}",
        ["PlainTextInMemorySecretKey"] = "InMemory MyKey Value",
        ["TransientFaultHandlingOptions:Enabled"] = bool.FalseString,
        ["Logging:LogLevel:Default"] = "Protect:{Warning}",
        ["UserDomain"] = "Protect:{DOMAIN\\USER}",
        ["EncryptedInMemorySpecialCharacters"] = "Protect:{\\!*+?|{[()^$.#}",
        ["PlainTextInMemorySpecialCharacters"] = "\\!*+?|{[()^$.#"
    };

    // define an environment variable to be encrypted
    Environment.SetEnvironmentVariable("EncryptedEnvironmentPassword", "Protect:{SecretEnvPassword\\!*+?|{[()^$.#}");
    Environment.SetEnvironmentVariable("PlainTextEnvironmentPassword", "SecretEnvPassword\\!*+?|{[()^$.#");

    // encrypts all configuration sources (must be done before reading the configuration)

    // encrypts all Protect:{<data>} token tags inside command line argument (you can use also the same method to encrypt String, IEnumerable<String>, IDictionary<String,String> value of any configuration source
    var encryptedArgs = standardProtectConfigurationData.ProtectConfigurationValue(args);

    // encrypts all Protect:{<data>} token tags inside im-memory dictionary
    magicPurposeStringProtectConfigurationData.ProtectConfigurationValue(memoryConfiguration);

    // encrypts all Protect:{<data>} token tags inside .json files and all OtherProtect:{<data>} inside .xml files 
    var encryptedJsonFiles = standardProtectConfigurationData.ProtectFiles(".");
    var encryptedXmlFiles = otherProtectedTokenizationProtectConfigurationData.ProtectFiles(".", searchPattern: "*.xml");

    // encrypts all Protect:{<data>} token tags inside environment variables
    magicPurposeStringProtectConfigurationData.ProtectEnvironmentVariables();

    // please check that all configuration source defined above are encrypted (check also Environment.GetEnvironmentVariable("SecretEnvironmentPassword") in Watch window)
    // note the per key purpose string override in file appsettings.development.json inside Nullable:DoubleArray contains two elements one with "Protect:{3.14}" and one with "Protect:{%customSubPurpose%}:{3.14}", even though the value is the same (3.14) they are encrypted differently due to the custom key purpose string
    // note the per key purpose string override in file appsettings.xml inside TransientFaultHandlingOptions contains two elements AutoRetryDelay with "OtherProtect:{00:00:07}" and AutoRetryDelaySubPurpose with "OtherProtect:{sUbPuRpOsE}:{00:00:07}", even though the value is the same (00:00:07) they are encrypted differently due to the custom key purpose string
    Debugger.Break();

    // define the application configuration using almost all possible known ConfigurationSources
    var configuration = new ProtectedConfigurationBuilder(standardProtectConfigurationData)  // global configuration
            .AddCommandLine(encryptedArgs)
            .AddJsonFile("appsettings.json")
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNETCORE_ENVIRONMENT")}.json", false, true)
            .AddXmlFile("appsettings.xml").WithProtectedConfigurationOptions(otherProtectedTokenizationProtectConfigurationData) // overrides global configuration for XML file
            .AddInMemoryCollection(memoryConfiguration).WithProtectedConfigurationOptions(magicPurposeStringProtectConfigurationData) // overrides global configuration for in-memory collection file
            .AddEnvironmentVariables().WithProtectedConfigurationOptions(magicPurposeStringProtectConfigurationData) // overrides global configuration for enviroment variables file
            .Build();

    // define other DI services: configure strongly typed AppSettings configuration class (must be done after having read the configuration)
    var services = new ServiceCollection();
    services.Configure<AppSettings>(configuration);
    var serviceProvider = services.BuildServiceProvider();

    // retrieve the strongly typed AppSettings configuration class, we use IOptionsMonitor in order to be notified on any reloads of appSettings
    var optionsMonitor = serviceProvider.GetRequiredService<IOptionsMonitor<AppSettings>>();
    var appSettings = optionsMonitor.CurrentValue;
    optionsMonitor.OnChange(appSettingsReloaded =>
    {
        // this breakpoint gets hit when the appsettings have changed due to a configuration reload, please check that the value of "Int" property inside appSettingsReloaded class is different from the one inside appSettings class
        // note that also there is an unavoidable framework bug on ChangeToken.OnChange which could get called multiple times when using FileSystemWatchers see https://github.com/dotnet/aspnetcore/issues/2542
        // see also the remarks section of FileSystemWatcher https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.created?view=net-8.0#remarks
        Console.WriteLine($"OnChangeEvent: appsettings.{Environment.GetEnvironmentVariable("DOTNETCORE_ENVIRONMENT")}.json has been reloaded! appSettings Int {appSettings.Int} appSettingsReloaded {appSettingsReloaded.Int}");
        Debugger.Break();
    });

    // please check that all values inside appSettings class are actually decrypted with the right value, make a note of the value of "Int" property it will change on the next second breakpoint
    Debugger.Break();

    // added some simple assertions to test that decrypted value is the same as original plaintext one
    Debug.Assert(appSettings.EncryptedCommandLinePassword == appSettings.PlainTextCommandLinePassword);
    Debug.Assert(appSettings.EncryptedEnvironmentPassword == appSettings.PlainTextEnvironmentPassword);
    Debug.Assert(appSettings.EncryptedInMemorySecretKey == appSettings.PlainTextInMemorySecretKey);

    // appsettings.json assertions
    Debug.Assert(appSettings.EncryptedJsonSpecialCharacters == appSettings.PlainTextJsonSpecialCharacters);
    Debug.Assert(appSettings.ConnectionStrings["PartiallyEncryptedConnectionString"].Contains("(local)\\SECONDINSTANCE"));
    Debug.Assert(appSettings.ConnectionStrings["PartiallyEncryptedConnectionString"].Contains("Secret_Catalog"));
    Debug.Assert(appSettings.ConnectionStrings["PartiallyEncryptedConnectionString"].Contains("secret_user"));
    Debug.Assert(appSettings.ConnectionStrings["PartiallyEncryptedConnectionString"].Contains("secret_password"));
    Debug.Assert(appSettings.ConnectionStrings["FullyEncryptedConnectionString"].Contains("Data Source=server1\\THIRDINSTANCE; Initial Catalog=DB name; User ID=sa; Password=pass5678; MultipleActiveResultSets=True;"));

    // appsettings.development.json assertions
    Debug.Assert(appSettings.Nullable.DateTime.Value.ToUniversalTime() == new DateTime(2016, 10, 1, 20, 34, 56, 789, DateTimeKind.Utc));
    Debug.Assert(appSettings.Nullable.Double == 123.456);
    Debug.Assert(appSettings.Nullable.Int == 98765);
    Debug.Assert(appSettings.Nullable.Bool == true);
    Debug.Assert(appSettings.Nullable.DoubleArray[1] == 3.14);
    Debug.Assert(appSettings.Nullable.DoubleArray[3] == 3.14);

    // appsettings.xml assertions
    Debug.Assert(appSettings.TransientFaultHandlingOptions["AutoRetryDelay"] == appSettings.TransientFaultHandlingOptions["AutoRetryDelaySubPurpose"]);
    Debug.Assert(appSettings.Logging.LogLevel["Microsoft"] == "Warning");
    Debug.Assert(appSettings.EncryptedXmlSecretKey == appSettings.PlainTextXmlSecretKey);


    // multiple configuration reload example (in order to check that the ReloadToken re-registration works)
    int i = 0;
    while (i++ < 5)
    {
        // updates inside appsettings.<environment>.json the property "Int": <whatever>, --> "Int": "Protected:{<random number>},"
        var environmentAppSettings = File.ReadAllText($"appsettings.{Environment.GetEnvironmentVariable("DOTNETCORE_ENVIRONMENT")}.json");
        environmentAppSettings = new Regex("\"Int\":.+?,").Replace(environmentAppSettings, $"\"Int\": \"{standardProtectConfigurationData.ProtectConfigurationValue($"Protect:{{{new Random().Next(0, 1000000)}}}")}\",");
        File.WriteAllText($"appsettings.{Environment.GetEnvironmentVariable("DOTNETCORE_ENVIRONMENT")}.json", environmentAppSettings);

        // wait 5 seconds for the reload to take place, please check on this breakpoint that the value of "Int" property has changed in appSettings class and it is the same of appSettingsReloaded
        Thread.Sleep(5000);
        appSettings = optionsMonitor.CurrentValue;
        Console.WriteLine($"ConfigurationReloadLoop: appSettings Int {appSettings.Int}");
        Debugger.Break();
    }
}

上面的代码非常简单且注释清晰,如果您在调试模式下启动它,它将使用 Debugger.Break() 自动在最重要的地方中断

  • 第一次发生在所有六个配置源值通过将默认标记 Protect:{<data to encrypt>} 替换为 Protected:{<encrypted data>} 进行加密之后(对于文件,请检查 bin/Debug/net6.0 文件夹中的文件,而不是解决方案目录中的文件,后者保持不变)
    • 通过使用提供的扩展方法 IProtectProviderConfigurationData.ProtectConfigurationValue,**命令行参数** args 被加密到 encryptedArgs 变量中。
    • **_appsettings._json** 文件已备份为 _.bak_ 文件,并使用提供的扩展方法 IProtectProviderConfigurationData.ProtectFiles 进行加密。
    • **appsettings.xml** 文件已备份为 .bak 文件,并已使用提供的扩展方法 IProtectProviderConfigurationData.ProtectFiles 进行加密
    • **环境变量** 已使用 IProtectProviderConfigurationData.ProtectEnvironmentVariables 加密。
  • 第二个实际上展示了 ProtectedConfigurationBuilder 的用法,如果您在调试器中查看 appSettings 强类型类,您会注意到它神奇地自动包含了具有正确数据类型的解密值,即使加密的键始终以 string 形式存储在配置源中。请注意,我使用了 IOptionsMonitor<AppSettings> 而不是 IOptions<AppSettings>,因为我还想测试在文件系统上更改配置文件时自动解密的功能。
     
  • 第三个、第五个、第七个等等,都在 IOptionsMonitor.OnChange 事件内部,发生在上一次将 "Int" 属性更新为 appsettings.development.json 文件中随机加密的整数之后(记下这个值,在下一个断点处您会再次需要它),您可以看到 appSettingsReloaded 变量的值与 appSettings 变量的值不同。
     
  • 第四个、第六个、第八个等等,都在 IOptionsMonitor.OnChange 事件之后,发生在从 IOptionsMonitor 将当前强类型配置类重新分配给 appSettings 变量之后,您可以检查 appSettings 包含与上一个断点处 appSettingsReloaded 变量相同的 "Int" 属性新值。

**所以总结一下,为了使用这个包,我们只需将调用 new ConfigurationBuilder() 替换为对 new ProtectedConfigurationBuilder() 的调用,传递 Data Protection API 配置和可能的自定义令牌化标签,一切都会以透明的方式完美运行。此外,所有的解密都发生在内存中,没有任何东西会出于任何原因存储在磁盘上。**

实现细节

我在此解释实现的主要要点

Fededim.Extensions.Configuration.Protected 包中的代码

  • ConfigurationBuilderExtensions 中定义的扩展方法
    • ProtectFilesIProtectProviderConfigurationData 的一个扩展方法,它会被调用并根据提供的扩展 searchPattern 在提供的 path 中扫描所有文件以查找 Protect:{<data to encrypt>} 标记,加密包含的数据,执行与 Protected:{<encrypted data>} 的替换,并在创建原始文件的可选备份(带有 .bak 扩展名)后将文件保存回去。同样,如果您不喜欢默认的标记化正则表达式,您可以传递自己的正则表达式,其约束是它必须将 <data to encrypt> 子字符串提取到一个名为 protectData 的组中,并且可选地将 <subPurposePattern><subPurpose> 子字符串分别提取到两个名为 subPurposePatternsubPurpose 的组中。
/// <summary>
/// Perform wildcard search of files in path encrypting any data using the specified <see cref="protectProviderConfigurationData"/>
/// </summary>
/// <param name="protectProviderConfigurationData">an IProtectProviderConfigurationData interface obtained from a one of the supported providers</param>
/// <param name="path">directory to be searched</param>
/// <param name="searchPattern">wildcard pattern to filter files</param>
/// <param name="searchOption">search options</param>
/// <param name="backupOriginalFile">boolean which indicates whether to make a backupof original file with extension .bak</param>
/// <returns>a list of filenames which have been successfully encrypted</returns>
/// <exception cref="ArgumentException"></exception>
public static IList<String> ProtectFiles(this IProtectProviderConfigurationData protectProviderConfigurationData, String path, String searchPattern = "*.json", SearchOption searchOption = SearchOption.TopDirectoryOnly, bool backupOriginalFile = true)
{
    protectProviderConfigurationData.CheckConfigurationIsValid();

    var result = new List<String>();

    foreach (var f in Directory.EnumerateFiles(path, searchPattern, searchOption))
    {
        var fileContent = File.ReadAllText(f);
        var replacedContent = fileContent;

        foreach (var protectFileOption in ProtectFilesOptions)
            if (protectFileOption.FilenameRegex.Match(f).Success)
            {
                replacedContent = protectFileOption.ProtectFileProcessor.ProtectFile(fileContent, protectProviderConfigurationData.ProtectRegex, (value) => ProtectConfigurationValue(protectProviderConfigurationData, value));
                break;
            }

        if (replacedContent != fileContent)
        {
            if (backupOriginalFile)
                File.Copy(f, f + ".bak", true);

            File.WriteAllText(f, replacedContent);

            result.Add(f);
        }
    }

    return result;
}

输入文件的格式根据公共静态属性 ConfigurationBuilderExtensions.ProtectFilesOptions 中指定的处理器进行解码、加密和重新编码, 默认情况下提供了三个处理器,两个用于 **JSON** 文件(例如 \\ 变成 \ 等,请参阅 JsonProtectFileProcessorJsonWithCommentsProtectFileProcessor 类在 ProtectFileProcessors.cs 中),一个用于 **XML** 文件(例如 &gt; 变成 > 等,请参阅 XmlProtectFileProcessor 类也在 ProtectFileProcessors.cs 中),一个用于 **RAW 文本文件**(不进行解码,请参阅 RawProtectFileProcessor 类也在 ProtectFileProcessors.cs 中)。您可以通过向 ConfigurationBuilderExtensions.ProtectFilesOptions 列表添加记录来添加任意数量的处理器,指定一个 filenameRegex,如果匹配,则应用关联的 ProtectFileProcessor,例如一个必须实现 IProtectFileProcessor 接口的类(见下文)。此接口本质上只有一个方法 ProtectFile,它本质上接受由 ConfigurationBuilderExtensions.ProtectFiles 方法传递的 3 个参数作为输入

  • rawFileText:这是原始输入文件作为字符串
  • protectRegex:这是配置的受保护正则表达式,必须与文件配置值匹配才能选择是否加密数据。
  • protectFunction:这是加密函数,它将纯文本数据作为输入并生成加密的 base64 字符串作为输出。

ProtectFile 方法必须对输入文件进行解码、加密和重新编码,并将其作为最终输出字符串返回。请注意,所有文件处理器都按 **FIFO (First-In First-Out)** 顺序处理,因此在配置附加处理程序时请记住这一点。

/// <summary>
/// This is the interface which must be implemented by a custom ProtectFileProcessor. It contains a single method <see cref="ProtectFile"/> used to decode, encrypt and re-encode the input file and return it as string.
/// </summary>
public interface IProtectFileProcessor
{
    /// <summary>
    /// This method actually implements a custom ProtectFileProcessor which must decode, encrypt and re-encode the input file and return it as string.
    /// </summary>
    /// <param name="rawFileText">The is the raw input file as a string</param>
    /// <param name="protectRegex">This is the configured protected regex which must be matched in file values in order to choose whether to encrypt or not the data.</param>
    /// <param name="protectFunction">This is the protect function taking the plaintext data as input and producing encrypted base64 data as output</param>
    /// <returns>the encrypted re-encoded file as a string</returns>
    String ProtectFile(String rawFileText, Regex protectRegex, Func<String, String> protectFunction);
}
  • ProtectEnvironmentVariablesIProtectProviderConfigurationData 的一个扩展方法,它以相同的标准加密环境变量。
    /// <summary>
    /// Encrypts all the environment variables using the specified <see cref="protectProviderConfigurationData"/> (used for environment variables)
    /// </summary>
    /// <param name="protectProviderConfigurationData">an IProtectProviderConfigurationData interface obtained from a one of the supported providers</param>
    public static void ProtectEnvironmentVariables(this IProtectProviderConfigurationData protectProviderConfigurationData, EnvironmentVariableTarget environmentTarget = EnvironmentVariableTarget.User)
    {
        var environmentVariables = Environment.GetEnvironmentVariables(environmentTarget);
    
        foreach (String key in environmentVariables.Keys)
            Environment.SetEnvironmentVariable(key, protectProviderConfigurationData.ProtectConfigurationValue(environmentVariables[key].ToString()));
    }
  • ProtectConfigurationValueIProtectProviderConfigurationData 的一个扩展方法,它以相同的标准加密 stringIEnumerable<string>string[]Dictionary<string,string>。负责实际加密的最终方法是 ProtectConfigurationValueInternal(它是唯一的私有方法,因为静态类没有受保护的成员)
/// <summary>
/// Encrypts the String value using the specified <see cref="protectProviderConfigurationData"/>
/// </summary>
/// <param name="protectProviderConfigurationData">an IProtectProviderConfigurationData interface obtained from a one of the supported providers</param>
/// <param name="value">a String literal which needs to be encrypted</param>
/// <returns>the encrypted configuration value</returns>
/// <exception cref="ArgumentException"></exception>
public static String ProtectConfigurationValue(this IProtectProviderConfigurationData protectProviderConfigurationData, String value)
{
    return ProtectConfigurationValueInternal(protectProviderConfigurationData, value);
}



/// <summary>
/// internal method actually performing the encryption using the specified <see cref="protectProviderConfigurationData"/>
/// </summary>
/// <param name="protectProviderConfigurationData">an IProtectProviderConfigurationData interface obtained from a one of the supported providers</param>
/// <param name="value">a String literal which needs to be encrypted</param>
/// <returns></returns>
private static String ProtectConfigurationValueInternal(IProtectProviderConfigurationData protectProviderConfigurationData, String value)
{
    if (value == null)
        return null;

    protectProviderConfigurationData.CheckConfigurationIsValid();

    return protectProviderConfigurationData.ProtectRegex.Replace(value, (me) =>
    {
        var subPurposePresent = !String.IsNullOrEmpty(me.Groups["subPurpose"]?.Value);

        var protectProvider = protectProviderConfigurationData.ProtectProvider;

        if (subPurposePresent)
            protectProvider = protectProviderConfigurationData.ProtectProvider.CreateNewProviderFromSubkey(me.Groups["subPurpose"].Value);

        return protectProviderConfigurationData.ProtectedReplaceString.Replace("${subPurposePattern}", subPurposePresent ? me.Groups["subPurposePattern"].Value : String.Empty).Replace("${protectedData}", protectProvider.Encrypt(me.Groups["protectData"].Value));
    });
}



/// <summary>
/// Encrypts the Dictionary<String, String> initialData using the specified <see cref="protectProviderConfigurationData"/> (used for in-memory collections)
/// </summary>
/// <param name="protectProviderConfigurationData">an IProtectProviderConfigurationData interface obtained from a one of the supported providers</param>
/// <param name="initialData">a Dictionary<String, String> whose values need to be encrypted</param>
public static void ProtectConfigurationValue(this IProtectProviderConfigurationData protectProviderConfigurationData, Dictionary<String, String> initialData)
{
    if (initialData != null)
        foreach (var key in initialData.Keys.ToList())
            initialData[key] = protectProviderConfigurationData.ProtectConfigurationValue(initialData[key]);
}



/// <summary>
/// Encrypts the IEnumerable<String> arguments using the specified <see cref="protectProviderConfigurationData"/>
/// </summary>
/// <param name="protectProviderConfigurationData">an IProtectProviderConfigurationData interface obtained from a one of the supported providers</param>
/// <param name="arguments">a IEnumerable<String> whose elements need to be encrypted</param>
/// <returns>a newer encrypted IEnumerable<String></returns>
public static IEnumerable<String> ProtectConfigurationValue(this IProtectProviderConfigurationData protectProviderConfigurationData, IEnumerable<String> arguments)
{
    return arguments?.Select(argument => protectProviderConfigurationData.ProtectConfigurationValue(argument));
}



/// <summary>
/// Encrypts the String[] arguments using the specified <see cref="protectProviderConfigurationData"/> (used for command-line arguments)
/// </summary>
/// <param name="protectProviderConfigurationData">an IProtectProviderConfigurationData interface obtained from a one of the supported providers</param>
/// <param name="arguments">a String array whose elements need to be encrypted</param>
/// <returns>a newer encrypted String[] array</returns>
public static String[] ProtectConfigurationValue(this IProtectProviderConfigurationData protectProviderConfigurationData, String[] arguments)
{
    return arguments?.Select(argument => protectProviderConfigurationData.ProtectConfigurationValue(argument)).ToArray();
}
  • WithProtectedConfigurationOptions: 它是 IConfigurationBuilder 的一个扩展方法,它允许覆盖特定 ConfigurationSource(例如,最后添加的那个)的 Data Protection 或标记化标签配置。请注意,此方法有点 hacky:我无法更改 ProtectedConfigurationBuilder.Add 的返回类型,否则 IConfigurationBuilder 接口将无法实现;因此 WithProtectedConfigurationOptions 扩展了标准 IConfigurationBuilder 接口,并将其转换为 IProtectedConfigurationBuilder 接口,并调用 ProtectedConfigurationBuilder.WithProtectedConfigurationOptions 方法,如果提供的 IConfigurationBuilder 不是 IProtectedConfigurationBuilder 的实例,它会引发异常,提醒将 new ConfigurationBuilder 实例化替换为 new ProtectedConfigurationBuilder。此方法只接受一个参数:一个正确配置的 IProtectProviderConfigurationData 类,它会覆盖 ProtectedConfigurationBuilder 构造函数中指定的全局配置。
    /// <summary>
    /// WithProtectedConfigurationOptions is a helper method used to override the ProtectedGlobalConfigurationData for a particular provider (e.g. the last one added)
    /// </summary>
    /// <param name="configurationBuilder">the IConfigurationBuilder instance</param>
    /// <param name="protectProviderLocalConfigurationData">a regular expression which captures the data to be decrypted in a named group called protectedData</param>
    /// <returns>The <see cref="IConfigurationBuilder"/> interface for method chaining</returns>
    /// <exception cref="ArgumentException">if configurationBuilder is not an instance of ProtectedConfigurationBuilder class</exception>
    public static IConfigurationBuilder WithProtectedConfigurationOptions(this IConfigurationBuilder configurationBuilder, IProtectProviderConfigurationData protectProviderLocalConfigurationData)
    {
        var protectedConfigurationBuilder = configurationBuilder as IProtectedConfigurationBuilder;
    
        if (protectedConfigurationBuilder != null)
            return protectedConfigurationBuilder.WithProtectedConfigurationOptions(protectProviderLocalConfigurationData);
        else
            throw new ArgumentException("Please use ProtectedConfigurationBuilder instead of ConfigurationBuilder class!", nameof(configurationBuilder));
    
    }
    
  • ProtectedConfigurationBuilder 实现了 IConfigurationBuilder 接口,就像 ConfigurationBuilder 框架类一样(部分实现借鉴自它),主要区别在于 Build 方法,它通过将 IConfigurationSource.Build 方法返回的 IConfigurationProvider 作为构造函数参数传递给负责内存中透明解密的核心类:ProtectedConfigurationProvider,从而实现**通过组合进行代理**。它还执行为被转换为 IConfigurationProviderIConfigurationSource 指定的自定义配置(如果您想知道如何执行,请查看 ProtectProviderConfigurationData.Merge static 方法)与 ProtectedConfigurationBuilder 构造函数中指定的全局配置之间的合并。
/// <summary>
/// Builds an <see cref="IConfiguration"/> with keys and values from the set of configuration sources registered in <see cref="Sources"/>.
/// </summary>
/// <returns>An <see cref="IConfigurationRoot"/> with keys and values from the providers generated by registered configuration sources.</returns>
public virtual IConfigurationRoot Build()
{
    var providers = new List<IConfigurationProvider>();
    foreach (IConfigurationSource source in _sources)
    {
        IConfigurationProvider provider = source.Build(this);

        // if we have a custom configuration we move the index from the ConfigurationSource object to the newly created ConfigurationProvider object
        ProtectProviderLocalConfigurationData.TryGetValue(source.GetHashCode(), out var protectedConfigurationData);
        if (protectedConfigurationData != null)
        {
            ProtectProviderLocalConfigurationData[provider.GetHashCode()] = protectedConfigurationData;
            ProtectProviderLocalConfigurationData.Remove(source.GetHashCode());
        }

        providers.Add(CreateProtectedConfigurationProvider(provider));
    }
    return new ConfigurationRoot(providers);
}




/// <summary>
/// CreateProtectedConfigurationProvider creates a new ProtectedConfigurationProvider using the composition approach
/// </summary>
/// <param name="provider">an existing IConfigurationProvider to instrument in order to perform the decryption of the encrypted keys</param>
/// <returns>a newer decrypted <see cref="IConfigurationProvider"/> if we have a valid protected configuration data, otherwise it returns the existing original undecrypted provider</returns>
protected virtual IConfigurationProvider CreateProtectedConfigurationProvider(IConfigurationProvider provider)
{
    // this code is an initial one of when I was thinking of casting IConfigurationProvider to ConfigurationProvider (all MS classes derive from this one)
    // in order to retrieve all configuration keys inside DecryptChildKeys using the Data property (through reflection since it is protected) without using the recursive "hack" of GetChildKeys 
    // it has been commented because it is not needed anymore, but I keep it as workaround for accessing all configuration keys just in case MS changes the implementation of GetChildKeys "forbidding" the actual way
    //var providerType = provider.GetType();

    //if (!providerType.IsSubclassOf(typeof(ConfigurationProvider)))
    //    return provider;

    // we merge ProtectedProviderGlobalConfigurationData and ProtectProviderLocalConfigurationData
    var actualProtectedConfigurationData = ProtectProviderLocalConfigurationData.ContainsKey(provider.GetHashCode()) ? ProtectProviderConfigurationData.Merge(ProtectedProviderGlobalConfigurationData, ProtectProviderLocalConfigurationData[provider.GetHashCode()]) : ProtectedProviderGlobalConfigurationData;

    // we use composition to perform decryption of all provider values
    return new ProtectedConfigurationProvider(provider, actualProtectedConfigurationData);
}
  • IProtectProviderConfigurationData 此类是一个抽象类,用于指定配置选项并将提供程序插入到 ProtectedConfigurationBuilder 中。
    /// <summary>
    /// an abstract class for specifying the configuration data of the encryption/decryption provider
    /// </summary>
    public abstract class IProtectProviderConfigurationData
    {
        public const String DefaultProtectRegexString = "Protect(?<subPurposePattern>(:{(?<subPurpose>[^:}]+)})?):{(?<protectData>.*?)}";
        public const String DefaultProtectedRegexString = "Protected(?<subPurposePattern>(:{(?<subPurpose>[^:}]+)})?):{(?<protectedData>.*?)}";
        public const String DefaultProtectedReplaceString = "Protected${subPurposePattern}:{${protectedData}}";
    
        /// <summary>
        /// The actual provider performing the encryption/decryption, <see cref="IProtectProvider"/> interface
        /// </summary>
        public IProtectProvider ProtectProvider { get; protected set; }
    
        /// <summary>
        /// a regular expression which captures the data to be decrypted in a named group called protectData
        /// </summary>
        public Regex ProtectedRegex { get; protected set; }
    
    
        /// <summary>
        /// a regular expression which captures the data to be encrypted in a named group called protectData
        /// </summary>
        public Regex ProtectRegex { get; protected set; }
    
    
        /// <summary>
        /// a string replacement expression which captures the substitution which must be applied for transforming unencrypted tokenization <see cref="DefaultProtectRegexString" /> into an encrypted tokenization <see cref="DefaultProtectedRegexString" />
        /// </summary>
        public String ProtectedReplaceString { get; protected set; }
    
    
        /// <summary>
        /// A helper overridable method for checking that the configuation data is valid (e.g. ProtectProvider is not null, ProtectedRegex and ProtectRegex contains both a regex group named protectedData) 
        /// </summary>
        public virtual void CheckConfigurationIsValid()
        {
            ProtectRegex = ProtectRegex ?? new Regex(DefaultProtectRegexString);
            if (!ProtectRegex.GetGroupNames().Contains("protectData"))
                throw new ArgumentException("ProtectRegex must contain a group named protectedData!", nameof(ProtectRegex));
    
            ProtectedRegex = ProtectedRegex ?? new Regex(DefaultProtectedRegexString);
            if (!ProtectedRegex.GetGroupNames().Contains("protectedData"))
                throw new ArgumentException("ProtectedRegex must contain a group named protectedData!", nameof(ProtectedRegex));
    
            ProtectedReplaceString = !String.IsNullOrEmpty(ProtectedReplaceString) ? ProtectedReplaceString : DefaultProtectedReplaceString;
            if (!ProtectedReplaceString.Contains("${protectedData}"))
                throw new ArgumentException("ProtectedReplaceString must contain ${protectedData}!", nameof(ProtectedReplaceString));
    
            if (ProtectProvider == null)
                throw new ArgumentException("ProtectProvider must not be null!", nameof(ProtectProvider));
        }
    }
    

    我们可以看到 IProtectProviderConfigurationData 主要包含四个属性和一个方法

    • ProtectedRegex:它是一个正则表达式,用于指定包含要解密的加密数据的标记;它必须定义一个名为 protectedData 的命名组(以及可选的两个名为 subPurposePatternsubPurpose 的附加组,用于指定每个配置值的子键)。如果为 null,则此参数采用默认值
      public const String DefaultProtectedRegexString = "Protected(?<subPurposePattern>(:{(?<subPurpose>[^:}]+)})?):{(?<protectedData>.*?)}";
      

      上述正则表达式本质上以惰性方式(因此它可以检索值内的所有出现)搜索与模式 'Protected:{<subPurpose>}:{<encrypted data>}' 匹配的任何 string,并提取 <encrypted data> 子字符串,将其存储在一个名为 protectedData 的组中。还有一个**可选**部分,称为 <subPurposePattern>(由 :{<subPurpose>} 组成),它允许指定每个配置值派生的子密钥(称为“子用途”,最初借鉴自 Data Protection API)。如果您不喜欢这种标记化,您可以用任何您喜欢的其他标记化替换它,方法是创建一个正则表达式,其限制是它必须将 <encrypted data> 子字符串提取到一个名为 protectedData 的组中,并将 <subPurposePattern><subPurpose> 子字符串提取到分别名为 subPurposePatternsubPurpose 的两个组中。

    • ProtectRegex:它是一个正则表达式,用于指定包含要加密数据的标记;同样,它必须定义一个名为 protectData 的命名组(以及可选的两个名为 subPurposePatternsubPurpose 的附加组,用于指定每个配置值的子密钥)。如果为 null,则此参数采用默认值(例如 Protect:{<subPurpose>}:{<data to be encrypted>}

      public const String DefaultProtectRegexString = "Protect(?<subPurposePattern>(:{(?<subPurpose>[^:}]+)})?):{(?<protectData>.*?)}";
      
    • DefaultProtectedReplaceString: 它是一个字符串表达式,用于将纯文本标记转换为加密标记(例如,从 Protect:{<subPurpose>}:{<data to be encrypted>} 转换为 Protected:{<subPurpose>}:{<encrypted data>})。它包含两个占位符 ${subPurposePattern}${protectedData},它们分别替换为 subPurposePattern(如果存在)和加密数据。如果为 null,此参数将采用默认值

public const String DefaultProtectedReplaceString = "Protected${subPurposePattern}:{${protectedData}}";
  • ProtectProvider:一个实现了 IProtectProvider 接口的类,用于指定可插拔的加密/解密逻辑。此接口由三个方法组成
    • Encrypt:它接受一个纯文本字符串并返回一个加密的 base64 字符串。
    • Decrypt:它接受一个 base64 加密字符串并返回纯文本字符串

    • CreateNewProviderFromSubkey:用于支持按配置值加密派生的子密钥。它接受一个子密钥字符串参数,该参数应用作加密子密钥以创建新的派生 IProtectProvider 接口。

/// <summary>
/// A standard interface which must implement by an encryption/decryption provider
/// </summary>
public interface IProtectProvider
{
    /// <summary>
    /// This method encrypts a plain-text string 
    /// </summary>
    /// <param name="plainTextValue">the plain-text string to be encrypted</param>
    /// <returns>the encrypted string</returns>
    String Encrypt(String plainTextValue);


    /// <summary>
    /// This method decrypts an encrypted string 
    /// </summary>
    /// <param name="encryptedValue">the encrypted string to be decrypted</param>
    /// <returns>the decrypted string</returns>
    String Decrypt(String encryptedValue);


    /// <summary>
    /// This methods create a new <see cref="IProtectProvider"/> for supporting per configuration value encryption subkey (e.g. "subpurposes")
    /// </summary>
    /// <param name="subkey">the per configuration value encryption subkey</param>
    /// <returns>a derived <see cref="IProtectProvider"/> based on the <see cref="subkey"/> parameter</returns>
    IProtectProvider CreateNewProviderFromSubkey(string subkey);
}
  • CheckConfigurationIsValid:这是一个基本的虚方法(即可重写方法),它主要检查 IProtectProviderConfigurationData 配置是否有效,如果无效则会引发带有详细信息的异常。每次您向 ProtectedConfigurationBuilder 添加 IProtectProviderConfigurationData 时都会调用它,无论是在构造函数中全局添加还是通过扩展方法 WithProtectedConfigurationOptions 局部添加。此外,在执行 ProtectProviderConfigurationData.Merge 以检查生成的配置是否有效时,以及在使用 IProtectProviderConfigurationData.Protect... 方法执行加密时,也会调用它。
  • ProtectedConfigurationProvider:此类的作用是对存储在任何现有配置源中的加密配置值进行实际的透明解密。它甚至应该支持未来的配置源,只要 Microsoft.Extensions.Configuration.ConfigurationProvider 的 GetChildKeys 实现不变(我主要用它来枚举所有可能的配置键)。本质上,它在其构造函数中将需要解密的 IConfigurationProvider 作为输入,并充当代理
    • 它通过在输入 IConfigurationProvider.Load 之后调用负责实际解密的方法 DecryptChildKeys 来重新定义 Load 方法。此方法最初使用 IConfigurationProvider 接口提供的标准方法 GetChildKeys 来枚举所有现有配置键,并通过匹配存储在 ProtectProviderConfigurationData 中的 ProtectedRegex 并使用 ProtectProviderDecrypt 方法来解密它们。如果正则表达式中存在 subPurpose 组,则使用 CreateNewProviderFromSubkey 方法创建一个临时派生的 ProtectProvider 并用于执行解密。在包的 1.0.15 版本中,我添加了一个更快的方法来解密子键,它仅使用**反射**来检索 ConfigurationProvider.Data 保护属性中的键值字典(应尽可能避免反射,但在这种情况下需要,因为该属性不是公共的,此外它仅用于检索字典,而不是键值条目)。速度提高了大约 3000 倍(是的,3 千倍!),并且应该得到所有 Microsoft 提供的 ConfigurationProviders 的支持(例如 XmlConfigurationProviderJsonConfigurationProviderMemoryConfigurationProviderCommandLineConfigurationProviderEnvironmentVariablesConfigurationProvider)。此方法有些粗糙,但它以安全模式使用,即如果正在解密的 IConfigurationProvider 对象不是 ConfigurationProvider 类的实例且该类不包含名为 Data 的属性,则使用旧的较慢的递归方法。

      /// <summary>
      /// Calls the underlying provider Load method in order to 
      /// load configuration values and then decrypts them by calling 
      /// DecryptChildKeys helper method
      /// </summary>
      public virtual void Load()
      {
          Provider.Load();
      
          // call DecryptChildKeys after Load
          DecryptChildKeys();
      } 
      
      
      
      
      /// <summary>
      /// Static PropertyInfo of protected property Data of Microsoft.Extensions.Configuration.ConfigurationProvider class (even though it is protected and not available here, you can use reflection in order to retrieve its value)
      /// </summary>
      public static PropertyInfo ConfigurationProviderDataProperty = typeof(ConfigurationProvider).GetProperty("Data", BindingFlags.NonPublic | BindingFlags.Instance);
      
      
      
      /// <summary>
      /// Hacky and fastest, tough safe method which gives access to the provider Data dictionary in readonly mode (it could be null in the future or for other providers not deriving from ConfigurationProvider, be sure to always check that it is not null!)
      /// </summary>
      public IReadOnlyDictionary<String, String> ProviderDataReadOnly
      {
          get
          {
              IDictionary<String, String> providerData = ProviderData;
      
              if (providerData != null)
                  return new ReadOnlyDictionary<String, String>(providerData);
      
              return null;
          }
      }
      
      
      
      
      /// <summary>
      /// Hacky and fastest, tough safe method which gives access to the provider Data dictionary (it could be null in the future or for other providers not deriving from ConfigurationProvider, be sure to always check that it is not null!)
      /// </summary>
      protected IDictionary<String, String> ProviderData
      {
          get
          {
              IDictionary<String, String> providerData = null;
      
              if (Provider is ConfigurationProvider && ConfigurationProviderDataProperty != null)
                  providerData = ConfigurationProviderDataProperty.GetValue(Provider) as IDictionary<string, string>;
      
              return providerData;
          }
      }
      
      
      
      /// <summary>
      /// This is a helper method actually responsible for the decryption of all configuration values. It decrypts all values using just IConfigurationBuilder interface methods so it should work on any existing or even future IConfigurationProvider <br /><br />
      /// Note: unluckily there Data dictionary property of ConfigurationProvider is not exposed on the interface IConfigurationProvider, but we can manage to get all keys by using the GetChildKeys methods, look at its implementation <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationProvider.cs#L61-L94"/> <br /><br />
      /// The only drawback of this method is that it returns the child keys of the level of the hierarchy specified by the parentPath parameter (it's at line 84 in MS source code "Segment(kv.Key, parentPath.Length + 1)" <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationProvider.cs#L84"/>) <br />
      /// So you have to use a recursive function to gather all existing keys and also to issue a distinct due to the way the GetChildKeys method has been implemented <br />
      /// </summary>
      /// <param name="parentPath"></param>
      protected void DecryptChildKeys(String parentPath = null)
      {
          IDictionary<String, String> dataProperty;
      
          // this is a hacky yet safe way to speed up key enumeration
          // we access the Data dictionary of ConfigurationProvider using reflection avoiding enumerating all keys with recursive function
          // the speed improvement is more 3000 times!
          if (((dataProperty = ProviderData) != null))
          {
              foreach (var key in dataProperty.Keys.ToList())
              {
                  if (!String.IsNullOrEmpty(dataProperty[key]))
                      Provider.Set(key, ProtectProviderConfigurationData.ProtectedRegex.Replace(dataProperty[key], me =>
                      {
      
                          var subPurposePresent = !String.IsNullOrEmpty(me.Groups["subPurpose"]?.Value);
      
                          IProtectProvider protectProvider = ProtectProviderConfigurationData.ProtectProvider;
      
                          if (subPurposePresent)
                              protectProvider = protectProvider.CreateNewProviderFromSubkey(me.Groups["subPurpose"].Value);
      
                          return protectProvider.Decrypt(me.Groups["protectedData"].Value);
                      }));
              }
          }
          else
          {
              foreach (var key in Provider.GetChildKeys(new List<String>(), parentPath).Distinct())
              {
                  var fullKey = parentPath != null ? $"{parentPath}:{key}" : key;
                  if (Provider.TryGet(fullKey, out var value))
                  {
                      if (!String.IsNullOrEmpty(value))
                          Provider.Set(fullKey, ProtectProviderConfigurationData.ProtectedRegex.Replace(value, me =>
                          {
      
                              var subPurposePresent = !String.IsNullOrEmpty(me.Groups["subPurpose"]?.Value);
      
                              IProtectProvider protectProvider = ProtectProviderConfigurationData.ProtectProvider;
      
                              if (subPurposePresent)
                                  protectProvider = protectProvider.CreateNewProviderFromSubkey(me.Groups["subPurpose"].Value);
      
                              return protectProvider.Decrypt(me.Groups["protectedData"].Value);
                          }));
                  }
                  else DecryptChildKeys(fullKey);
              }
          }
      }
      

      如果底层 IConfigurationProvider 支持,它将创建自己的 ReloadToken,在 GetReloadToken 方法中返回它,最后使用框架静态实用方法 ChangeToken.OnChange 类向输入 IConfigurationProvider 重新加载令牌注册回调,以便在配置更改时收到通知,重新执行解密以支持在配置重新加载时自动解密值

      public ProtectedConfigurationProvider(IConfigurationProvider provider, ProtectedConfigurationData protectedConfigurationData)
      {
          Provider = provider;
          ProtectedConfigurationData = protectedConfigurationData;
      
          RegisterReloadCallback();
      }
      
      /// <summary>
      /// Registers a reload callback which redecrypts all values if the underlying IConfigurationProvider supports it
      /// </summary>
      protected void RegisterReloadCallback()
      {
          // check if underlying provider supports reloading
          if (Provider.GetReloadToken() != null)
          {
              // Create our reload token
              ReloadToken = new ConfigurationReloadToken();
      
              // registers Provider on Change event using framework static utility method ChangeToken.OnChange in order to be notified of configuration reload and redecrypts subsequently the needed keys
              ProviderReloadTokenRegistration = ChangeToken.OnChange(() => Provider.GetReloadToken(), (configurationProvider) =>
              {
                  var protectedConfigurationProvider = configurationProvider as ProtectedConfigurationProvider;
      
                  // redecrypts all needed keys
                  protectedConfigurationProvider.DecryptChildKeys();
      
                  // notifies all subscribes
                  OnReload();
              }, this);
          }
      }
      
      /// <summary>
      /// Returns our reload token
      /// </summary>
      /// <returns>the <see cref="ReloadToken"/></returns>
      public IChangeToken GetReloadToken()
      {
          return ReloadToken;
      }
      
      /// <summary>
      /// Dispatches all the callbacks waiting for the reload event from 
      /// this configuration provider (and creates a new ReloadToken)
      /// </summary>
      protected virtual void OnReload()
      {
          ConfigurationReloadToken previousToken = 
            Interlocked.Exchange(ref ReloadToken, new ConfigurationReloadToken());
          previousToken.OnReload();
      }

Fededim.Extensions.Configuration.Protected.DataProtectionAPI 包中的代码

  • DataProtectionAPIProtectConfigurationData:此类的主要作用是存储 Data Protection API 的全局配置或 ConfigurationSource 特定的配置,以及可能的自定义标记化正则表达式。它还在主构造函数中设置了另一个依赖注入提供程序(原因见上文),并提供了两个重载,一个用于 keyNumber,一个用于 purposeString。除了这两个主要构造函数外,还提供了其他几个重载以提高可用性。
    /// <summary>
    /// Main constructor for DataProtectionAPIProtectConfigurationData using a key number
    /// </summary>
    /// <param name="protectRegexString">a regular expression which captures the data to be encrypted in a named group called protectData</param>
    /// <param name="protectedRegexString">a regular expression which captures the data to be decrypted in a named group called protectedData</param>
    /// <param name="protectedReplaceString">a string replacement expression which captures the substitution which must be applied for transforming unencrypted tokenization into an encrypted tokenization</param>
    /// <param name="dataProtectionServiceProvider">a service provider configured with Data Protection API, this parameters is mutually exclusive to dataProtectionConfigureAction</param>
    /// <param name="dataProtectionConfigureAction">a configure action to setup the Data Protection API, this parameters is mutually exclusive to dataProtectionServiceProvider</param>
    /// <param name="keyNumber">a number specifying the index of the key to use</param>
    /// <exception cref="ArgumentException">if dataProtectionServiceProvider or dataProtectionServiceProvider is null or not well configured</exception>
    public DataProtectionAPIProtectConfigurationData(String protectRegexString = null, String protectedRegexString = null, String protectedReplaceString = null, IServiceProvider dataProtectionServiceProvider = null, Action<IDataProtectionBuilder> dataProtectionConfigureAction = null, int keyNumber = 1)
        : this(protectRegexString, protectedRegexString, protectedReplaceString, dataProtectionServiceProvider, dataProtectionConfigureAction, DataProtectionAPIProtectConfigurationKeyNumberToString(keyNumber))
    {
    
    }
    
    
    
    /// <summary>
    /// Main constructor for DataProtectionAPIProtectConfigurationData using a purpose string
    /// </summary>
    /// <param name="protectRegexString">a regular expression which captures the data to be encrypted in a named group called protectData</param>
    /// <param name="protectedRegexString">a regular expression which captures the data to be decrypted in a named group called protectedData</param>
    /// <param name="protectedReplaceString">a string replacement expression which captures the substitution which must be applied for transforming unencrypted tokenization into an encrypted tokenization</param>
    /// <param name="dataProtectionServiceProvider">a service provider configured with Data Protection API, this parameters is mutually exclusive to dataProtectionConfigureAction</param>
    /// <param name="dataProtectionConfigureAction">a configure action to setup the Data Protection API, this parameters is mutually exclusive to dataProtectionServiceProvider</param>
    /// <param name="purposeString">a string used to derive the encryption key</param>
    /// <exception cref="ArgumentException">if dataProtectionServiceProvider or dataProtectionServiceProvider is null or not well configured</exception>
    public DataProtectionAPIProtectConfigurationData(String protectRegexString = null, String protectedRegexString = null, String protectedReplaceString = null, IServiceProvider dataProtectionServiceProvider = null, Action<IDataProtectionBuilder> dataProtectionConfigureAction = null, String purposeString = null)
    {
        // check that at least one parameter is not null
        if (dataProtectionServiceProvider == null && dataProtectionConfigureAction == null)
            throw new ArgumentException("Either dataProtectionServiceProvider or dataProtectionConfigureAction must not be null!");
    
        // if dataProtectionServiceProvider is null and we pass a dataProtectionConfigureAction configure a new service provider
        if (dataProtectionServiceProvider == null && dataProtectionConfigureAction != null)
        {
            var services = new ServiceCollection();
            dataProtectionConfigureAction(services.AddDataProtection());
            dataProtectionServiceProvider = services.BuildServiceProvider();
        }
    
        // check that dataProtectionServiceProvider resolves the IDataProtector
        var dataProtect = dataProtectionServiceProvider.GetRequiredService<IDataProtectionProvider>().CreateProtector(DataProtectionAPIProtectConfigurationStringPurpose(purposeString));
        if (dataProtect == null)
            throw new ArgumentException("Either dataProtectionServiceProvider or dataProtectionConfigureAction must configure the DataProtection services!", dataProtectionServiceProvider == null ? nameof(dataProtectionServiceProvider) : nameof(dataProtectionConfigureAction));
    
        // sets the abstract class base properties and calls CheckConfigurationIsValid
        if (!String.IsNullOrEmpty(protectRegexString))
            ProtectRegex = new Regex(protectRegexString);
    
        if (!String.IsNullOrEmpty(protectedRegexString))
            ProtectedRegex = new Regex(protectedRegexString);
    
        ProtectedReplaceString = protectedReplaceString;
    
        ProtectProvider = new DataProtectionAPIProtectProvider(dataProtect);
    
        CheckConfigurationIsValid();
    }
  • DataProtectionAPIProtectProvider 它是使用 Microsoft Data Protection API 实现 IProtectProvider 接口的(例如,对 Decrypt 调用 IDataProtector.Unprotect,对 Encrypt 调用 IDataProtector.Protect,对 CreateNewProviderFromSubkey 调用 IDataProtector.CreateProtector)。

/// <summary>
/// The standard Microsoft DataProtectionAPI protect provider for Fededim.Extensions.Configuration.Protected, implementing the <see cref="IProtectProvider"/> interface.
/// </summary>
public class DataProtectionAPIProtectProvider : IProtectProvider
{
public IDataProtector DataProtector { get; }

/// <summary>
/// The main constructor
/// </summary>
/// <param name="dataProtector">the <see cref="IDataProtect"/> interface obtained from Data Protection API</param>
public DataProtectionAPIProtectProvider(IDataProtector dataProtector)
{
    DataProtector = dataProtector;
}

/// <summary>
/// This methods create a new <see cref="IProtectProvider"/> for supporting per configuration value encryption subkey (e.g. "subpurposes")
/// </summary>
/// <param name="subkey">the per configuration value encryption subkey</param>
/// <returns>a derived <see cref="IProtectProvider"/> based on the <see cref="subkey"/> parameter</returns>
public IProtectProvider CreateNewProviderFromSubkey(String subkey)
{
    return new DataProtectionAPIProtectProvider(DataProtector.CreateProtector(subkey));
}

/// <summary>
/// This method decrypts an encrypted string 
/// </summary>
/// <param name="encryptedValue">the encrypted string to be decrypted</param>
/// <returns>the decrypted string</returns>
public String Decrypt(String encryptedValue)
{
    return DataProtector.Unprotect(encryptedValue);
}

/// <summary>
/// This method encrypts a plain-text string 
/// </summary>
/// <param name="plainTextValue">the plain-text string to be encrypted</param>
/// <returns>the encrypted string</returns>
public String Encrypt(String plainTextValue)
{
    return DataProtector.Protect(plainTextValue);
}
}

关注点

我认为通过正则表达式指定自定义标签的想法非常巧妙,因为它为每个用户提供了自定义标记化标签所需的灵活性。

奇怪的是,MS 没有在 IConfigurationProvider 中规划一个方法或属性来枚举提供程序的所有可能键,可能(且希望)在未来版本中会添加,这样我就可以避免使用效率不高的递归 GetChildKeys 方法来枚举所有键。

如果你想知道是否可以在你的项目中使用这个包,并且它将来仍然可以工作,我可以强调,唯一的关键点是所有配置键的枚举,现在是通过使用 GetChildKeys 方法完成的。即使它的实现可能会被 Microsoft 更改,它也总是会提供其名称所指的功能,即配置键的子键。即使在极少数情况下,GetChildKeys 方法将从 IConfigurationProvider 接口中删除,你仍然可以通过将接口转换为 ConfigurationProvider 基类并通过反射访问受保护的 Data 属性来访问所有配置键(请参阅 CreateProtectedConfigurationProvider 方法中的注释,基本上所有配置提供程序都派生自这个类),所以我非常有信心这个包将可以使用很多年。

此外,自 1.0.12 版本以来,该软件包允许可插拔加密/解密,以防您出于任何原因(声称 Microsoft 隐藏后门,开源产品爱好者等)不使用基于 Microsoft Data Protection API 的默认提供程序 Fededim.Extensions.Configuration.Protected.DataProtectionAPI

此外,自 Fededim.Extensions.Configuration.Protected 版本 1.0.16 起,我实现了将正在解密的 IConfigurationProvider **安全地**转换为 ConfigurationProvider 基类,从而使子键枚举过程快得令人难以置信。在此之上,我最近添加了一个 xUnit 测试项目,以提高这两个包的可靠性和软件质量:在我的个人笔记本电脑(基于 Intel I9-13900K)上,我成功地在五个测试用例中测试了 2*100000 个随机键,每个测试用例都处理**框架提供的 ConfigurationProviders**(**CommandLineConfigurationProvider**、**EnvironmentVariablesConfigurationProvider**、**MemoryConfigurationProvider**、**JsonConfigurationProvider** _[两次传递,一次使用 JsonProtectFileProcessor,一次使用 JsonWithCommentsProtectFileProcessor]_、**XmlConfigurationProvider**)。例如,针对 JsonConfigurationProvider 的一个测试用例生成了一个总大小为 60MB 的纯文本文件和一个总大小为 91MB 的加密文件,该测试在大约 10 秒内完成了生成随机 JSON 文件、加密、使用 ProtectedConfigurationBuilder 解密_(为了解密 250k 个加密值,此步骤在 .Net462 中花费了大约 5 秒,在 net6.0 中花费了大约 3 秒,速度更快)_并检查每个解密键是否与纯文本键相等。**此外,所有五个测试用例的整个集合都重复了 1000 次**(Test Explorer Run Until Failure,不幸的是它不适用于整个测试套件,我必须单独执行),**对于 net462(总运行时间 705 分钟)**和 **net6.0(总运行时间 424 分钟)**,都没有引发任何错误,如下图所示。

Net462 耐力测试

Net6.0 耐力测试

最后但并非最不重要的一点,我想提醒大家,protected 成员访问修饰符确实存在!与 private 相比,我很少看到它被使用,有时甚至在 .NET 框架类中!它应该被用作默认的成员访问修饰符,几乎没有或根本没有 private 成员,因为它允许**继承**,而继承是面向对象编程的基础(你永远不知道谁想扩展和定制你的类以满足他们或其他一般需求)。

历史

  • V1.0.0 (2023 年 12 月 16 日)
    • 初始版本
  • V1.0.1 (2023 年 12 月 27 日)
    • ConfigurationBuilderExtensionsProtectedConfigurationBuilderProtectedConfigurationProvider 的实现细节部分添加了更多代码
    • 在实现细节部分更好地解释了配置重新加载时值的自动解密工作原理
  • V1.0.2 (2023 年 12 月 30 日)
    • 改进了 ConfigureDataProtection 的解释,并添加了 Data Protection 目的字符串文档的链接
    • 在“兴趣点”部分添加了关于 MS IConfigurationProvider 中缺少枚举所有键的方法或属性的要点
    • 修复了一些拼写错误
  • V1.0.3 (2024 年 2 月 7 日)
    • ProtectedConfigurationBuilderCreateProtectedConfigurationProvider 方法中注释掉了 3 行不需要的代码
    • 发布了 NuGet 包版本 1.0.5
  • V1.0.4 (2024 年 2 月 8 日)
    • 兴趣点部分添加了关于此包的关键点及其未来仍能工作的可能性的要点
    • 兴趣点部分添加了关于促进受保护成员访问修饰符使用的要点
  • V1.0.5 (2024 年 5 月 4 日)
    • 发布了 NuGet 包版本 1.0.6
    • 更新了段落和代码以反映代码更改(例如 ConfigurationBuilderExtensionsIDataProtect.ProtectFilesProtected.ConsoleTest
    • 添加了 appsetting.json、appsettings.development.json 和 appsettings.xml 文件的源代码超链接
  • V1.0.6 (2024 年 5 月 8 日)
    • 发布了 NuGet 包版本 1.0.7
    • 更新了段落和代码以反映代码更改(例如 ConfigurationBuilderExtensionsProtectedConfigurationProvider. RegisterReloadCallbackProtected.ConsoleTest
  • V1.0.7 (2024 年 5 月 31 日)
    • 发布了 NuGet 包版本 1.0.10
    • 更新了段落和代码以反映代码更改(例如 ConfigurationBuilderExtensions 中的 ProtectFilesProtectConfigurationValueWithProtectedConfigurationOptionsProtectedConfigurationProvider.DecryptChildKeysProtectedConfigurationData 构造函数、ProtectedConfigurationBuilder 构造函数和正则表达式字符串,添加了一个新文件 ProtectFileProcessors.cs,其中包含 IFileProtectProcessorFilesProtectOptionsRawFileProtectProcessorJsonFileProtectProcessorXmlFileProtectProcessor
  • V1.0.8 (2024 年 6 月 18 日)
    • 发布了 NuGet 包版本 1.0.13,并将 Data Protection API 加密/解密代码提取到一个名为 Fededim.Extensions.Configuration.Protected.DataProtectionAPI 的新提供程序包中
    • 更新了不同的段落和代码以反映代码更改(太多无法引用)
  • V1.0.9 (2024 年 6 月 26 日)
    • 发布了 Fededim.Extensions.Configuration.Protected NuGet 包版本 1.0.14 和 Fededim.Extensions.Configuration.Protected.DataProtectionAPI 版本 1.0.2
    • 更新了不同的段落和代码以反映代码更改
    • 添加了 xUnit 测试项目以提高可靠性和软件质量
  • V1.0.10 (2024 年 6 月 28 日)
    • 发布了 Fededim.Extensions.Configuration.Protected NuGet 包版本 1.0.16 和 Fededim.Extensions.Configuration.Protected.DataProtectionAPI 版本 1.0.4
    • 使子键枚举过程快得令人难以置信(如果提供程序派生自 ConfigurationProvider,就像所有 Microsoft 提供的 ConfigurationProviders 一样,否则使用旧方法)
    • 改进了 xUnit 测试项目的代码和速度
    • 修复了 ProtectEnvironmentVariables 上的错误(未传递目标环境)
  • V1.0.11 (2024 年 7 月 3 日)
    • 发布了 Fededim.Extensions.Configuration.Protected NuGet 包版本 1.0.17 和 Fededim.Extensions.Configuration.Protected.DataProtectionAPI 版本 1.0.5
    • 使用 as 而不是直接类型转换,使 ProtectedConfigurationProvider.ProviderData 属性更安全
    • 向各种方法添加了 virtual 以允许可扩展性
    • 更新了代码以反映代码更改
© . All rights reserved.