MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#配置系统(IConfiguration)扩展与加密

2022-02-162.5k 阅读

C# 配置系统(IConfiguration)基础

在 C# 开发中,IConfiguration 是一个至关重要的接口,它提供了一种统一的方式来管理应用程序的配置数据。无论是开发 Web 应用、控制台应用还是服务,配置数据都是必不可少的,例如数据库连接字符串、API 密钥等。

1. IConfiguration 体系结构

IConfiguration 接口位于 Microsoft.Extensions.Configuration 命名空间下。它通过一个树形结构来表示配置数据,每个节点都有一个键值对。这种结构使得我们可以很方便地组织和访问配置数据。

IConfigurationRoot 接口继承自 IConfiguration,通常是通过 ConfigurationBuilder 来构建的。ConfigurationBuilder 提供了一种链式调用的方式来添加不同的配置源。常见的配置源包括:

  • JSON 文件:JSON 文件是一种广泛使用的配置格式,因为它简洁明了且易于阅读和编写。在.NET 应用中,我们通常会有 appsettings.json 文件来存储应用程序的配置。
  • 环境变量:环境变量是操作系统提供的一种配置方式,通过设置环境变量,我们可以在不修改代码的情况下改变应用程序的行为。在部署应用时,环境变量尤其有用,例如设置数据库连接字符串等敏感信息。
  • 命令行参数:在启动应用程序时,可以通过命令行传递参数,这些参数也可以作为配置的一部分。这在调试或者临时改变应用行为时很方便。

2. 基本使用示例

下面是一个简单的控制台应用示例,展示如何从 appsettings.json 文件中读取配置数据。

首先,在项目中添加 Microsoft.Extensions.Configuration.Json 包,这是用于从 JSON 文件读取配置的包。

appsettings.json 文件中添加以下内容:

{
  "MySettings": {
    "Value1": "Hello World",
    "Value2": 42
  }
}

Program.cs 文件中编写如下代码:

using Microsoft.Extensions.Configuration;
using System;

class Program
{
    static void Main()
    {
        var builder = new ConfigurationBuilder()
           .SetBasePath(AppContext.BaseDirectory)
           .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

        IConfigurationRoot configuration = builder.Build();

        string value1 = configuration.GetSection("MySettings:Value1").Value;
        int value2 = int.Parse(configuration.GetSection("MySettings:Value2").Value);

        Console.WriteLine($"Value1: {value1}, Value2: {value2}");
    }
}

在上述代码中,我们首先创建了一个 ConfigurationBuilder,设置了基础路径并添加了 appsettings.json 文件作为配置源。然后构建 IConfigurationRoot,通过 GetSection 方法获取特定节点的值。

C# 配置系统(IConfiguration)扩展

虽然 IConfiguration 本身已经提供了强大的配置管理功能,但在实际项目中,我们经常需要对其进行扩展,以满足特定的业务需求。

1. 自定义配置源

有时候,我们可能需要从自定义的数据源读取配置,例如从数据库、自定义文件格式等。为了实现这一点,我们需要创建自定义的配置源和配置提供程序。

创建自定义配置源

首先,定义一个自定义配置源类,它需要实现 IConfigurationSource 接口。

using Microsoft.Extensions.Configuration;
using System;

public class MyCustomConfigurationSource : IConfigurationSource
{
    private readonly string _connectionString;

    public MyCustomConfigurationSource(string connectionString)
    {
        _connectionString = connectionString;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new MyCustomConfigurationProvider(_connectionString);
    }
}

在上述代码中,MyCustomConfigurationSource 接受一个连接字符串作为参数,并在 Build 方法中返回一个自定义的配置提供程序。

创建自定义配置提供程序

接着,定义自定义配置提供程序类,它需要继承自 ConfigurationProvider 类。

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;

public class MyCustomConfigurationProvider : ConfigurationProvider
{
    private readonly string _connectionString;

    public MyCustomConfigurationProvider(string connectionString)
    {
        _connectionString = connectionString;
    }

    public override void Load()
    {
        // 从自定义数据源加载配置数据,这里简单示例为硬编码数据
        Data = new Dictionary<string, string>
        {
            { "Custom:Setting1", "Value from custom source" },
            { "Custom:Setting2", "42" }
        };
    }
}

Load 方法中,我们从自定义数据源(这里简单示例为硬编码数据,实际应用中可能从数据库查询等)加载配置数据,并将其存储在 Data 属性中。

使用自定义配置源

在应用程序中使用自定义配置源:

using Microsoft.Extensions.Configuration;
using System;

class Program
{
    static void Main()
    {
        var builder = new ConfigurationBuilder()
           .Add(new MyCustomConfigurationSource("your_connection_string"));

        IConfigurationRoot configuration = builder.Build();

        string setting1 = configuration.GetSection("Custom:Setting1").Value;
        int setting2 = int.Parse(configuration.GetSection("Custom:Setting2").Value);

        Console.WriteLine($"Setting1: {setting1}, Setting2: {setting2}");
    }
}

通过 Add 方法将自定义配置源添加到 ConfigurationBuilder 中,然后就可以像使用其他配置源一样读取自定义配置数据。

2. 扩展方法

为了更方便地使用 IConfiguration,我们可以创建一些扩展方法。例如,假设我们经常需要获取特定类型的配置对象,而不仅仅是单个值。

using Microsoft.Extensions.Configuration;
using System;

public static class ConfigurationExtensions
{
    public static T GetSettings<T>(this IConfiguration configuration, string sectionName) where T : new()
    {
        var section = configuration.GetSection(sectionName);
        var settings = new T();
        section.Bind(settings);
        return settings;
    }
}

上述扩展方法 GetSettings 可以从指定的配置节中获取数据并绑定到指定类型的对象上。

使用示例:

using Microsoft.Extensions.Configuration;
using System;

class MySettings
{
    public string Value1 { get; set; }
    public int Value2 { get; set; }
}

class Program
{
    static void Main()
    {
        var builder = new ConfigurationBuilder()
           .SetBasePath(AppContext.BaseDirectory)
           .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

        IConfigurationRoot configuration = builder.Build();

        MySettings settings = configuration.GetSettings<MySettings>("MySettings");

        Console.WriteLine($"Value1: {settings.Value1}, Value2: {settings.Value2}");
    }
}

通过扩展方法,我们可以更简洁地获取和处理配置数据。

C# 配置系统(IConfiguration)加密

在应用程序中,配置数据可能包含敏感信息,如数据库密码、API 密钥等。为了保护这些信息,我们需要对配置数据进行加密。

1. 加密 JSON 配置文件

一种常见的做法是对 appsettings.json 文件中的敏感部分进行加密。我们可以在应用启动时解密这些部分,然后将解密后的数据注入到 IConfiguration 中。

使用 DataProtection API 加密

.NET 提供了 DataProtection API 来进行数据加密和解密。首先,在项目中添加 Microsoft.AspNetCore.DataProtection 包(虽然它是为 ASP.NET Core 设计的,但在其他类型的应用中也可以使用)。

using Microsoft.AspNetCore.DataProtection;
using System;

class EncryptionHelper
{
    private readonly IDataProtector _protector;

    public EncryptionHelper(IDataProtectionProvider provider, string purpose)
    {
        _protector = provider.CreateProtector(purpose);
    }

    public string Encrypt(string plaintext)
    {
        return _protector.Protect(plaintext);
    }

    public string Decrypt(string ciphertext)
    {
        return _protector.Unprotect(ciphertext);
    }
}

上述 EncryptionHelper 类使用 IDataProtectionProvider 创建一个 IDataProtector,用于加密和解密字符串。

加密配置数据

假设我们要加密 appsettings.json 中的数据库连接字符串。首先,在配置文件中存储加密后的数据:

{
  "ConnectionStrings": {
    "DefaultConnection": "ENC[AES256,IV=...,CipherText=...]"
  }
}

在应用启动时,解密数据并替换原有的加密数据:

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Text.Json;

class Program
{
    static void Main()
    {
        var dataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo("C:\\DataProtectionKeys"));
        var encryptionHelper = new EncryptionHelper(dataProtectionProvider, "MyPurpose");

        var builder = new ConfigurationBuilder()
           .SetBasePath(AppContext.BaseDirectory)
           .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

        IConfigurationRoot configuration = builder.Build();

        string encryptedConnectionString = configuration.GetSection("ConnectionStrings:DefaultConnection").Value;
        if (encryptedConnectionString.StartsWith("ENC["))
        {
            // 解密
            string decrypted = encryptionHelper.Decrypt(encryptedConnectionString.Substring(4, encryptedConnectionString.Length - 5));

            // 读取 JSON 文件内容
            string jsonContent = File.ReadAllText("appsettings.json");
            var jsonDocument = JsonDocument.Parse(jsonContent);
            var root = jsonDocument.RootElement;
            var connectionStrings = root.GetProperty("ConnectionStrings");
            var defaultConnection = connectionStrings.GetProperty("DefaultConnection");
            var jsonElement = JsonElement.Parse($"\"{decrypted}\"");
            var newRoot = JsonSerializer.Deserialize<JsonElement>(jsonContent.Replace(defaultConnection.ToString(), jsonElement.ToString()));

            // 写入解密后的 JSON 文件
            File.WriteAllText("appsettings.json", JsonSerializer.Serialize(newRoot, new JsonSerializerOptions { WriteIndented = true }));

            // 重新加载配置
            configuration = new ConfigurationBuilder()
               .SetBasePath(AppContext.BaseDirectory)
               .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
               .Build();
        }

        string connectionString = configuration.GetSection("ConnectionStrings:DefaultConnection").Value;
        Console.WriteLine($"Decrypted Connection String: {connectionString}");
    }
}

在上述代码中,我们首先创建了一个 EncryptionHelper 用于解密。然后读取配置文件中的加密连接字符串,如果是加密格式,则进行解密,并替换 JSON 文件中的加密内容,最后重新加载配置。

2. 加密环境变量

如果配置数据存储在环境变量中,同样可以对环境变量进行加密。

加密环境变量值

使用类似上述的加密方法,在设置环境变量之前对值进行加密:

using Microsoft.AspNetCore.DataProtection;
using System;

class Program
{
    static void Main()
    {
        var dataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo("C:\\DataProtectionKeys"));
        var encryptionHelper = new EncryptionHelper(dataProtectionProvider, "MyPurpose");

        string sensitiveValue = "my_secret_api_key";
        string encryptedValue = encryptionHelper.Encrypt(sensitiveValue);

        Environment.SetEnvironmentVariable("API_KEY", encryptedValue);
    }
}

在应用中解密环境变量

在应用启动时,从环境变量中读取加密值并解密:

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using System;

class Program
{
    static void Main()
    {
        var dataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo("C:\\DataProtectionKeys"));
        var encryptionHelper = new EncryptionHelper(dataProtectionProvider, "MyPurpose");

        var builder = new ConfigurationBuilder()
           .AddEnvironmentVariables();

        IConfigurationRoot configuration = builder.Build();

        string encryptedApiKey = configuration["API_KEY"];
        if (!string.IsNullOrEmpty(encryptedApiKey))
        {
            string decryptedApiKey = encryptionHelper.Decrypt(encryptedApiKey);
            Console.WriteLine($"Decrypted API Key: {decryptedApiKey}");
        }
    }
}

通过这种方式,我们可以保护存储在环境变量中的敏感配置数据。

结合扩展与加密

在实际项目中,我们可能需要同时使用配置系统的扩展和加密功能。例如,在自定义配置源中使用加密的数据,或者在扩展方法中处理解密后的配置数据。

1. 在自定义配置源中处理加密数据

假设我们的自定义配置源从数据库中读取配置数据,并且数据库中的某些字段是加密的。

自定义配置提供程序中的解密

MyCustomConfigurationProviderLoad 方法中进行解密:

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;

public class MyCustomConfigurationProvider : ConfigurationProvider
{
    private readonly string _connectionString;
    private readonly EncryptionHelper _encryptionHelper;

    public MyCustomConfigurationProvider(string connectionString, EncryptionHelper encryptionHelper)
    {
        _connectionString = connectionString;
        _encryptionHelper = encryptionHelper;
    }

    public override void Load()
    {
        // 从数据库读取配置数据,这里简单示例为硬编码加密数据
        Dictionary<string, string> encryptedData = new Dictionary<string, string>
        {
            { "Custom:Setting1", "ENC[AES256,IV=...,CipherText=...]" },
            { "Custom:Setting2", "ENC[AES256,IV=...,CipherText=...]" }
        };

        Data = new Dictionary<string, string>();
        foreach (var pair in encryptedData)
        {
            if (pair.Value.StartsWith("ENC["))
            {
                string decrypted = _encryptionHelper.Decrypt(pair.Value.Substring(4, pair.Value.Length - 5));
                Data.Add(pair.Key, decrypted);
            }
            else
            {
                Data.Add(pair.Key, pair.Value);
            }
        }
    }
}

在上述代码中,我们在 Load 方法中对从数据库读取的加密数据进行解密,并存储解密后的数据。

使用自定义配置源和加密

在应用程序中使用:

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using System;

class Program
{
    static void Main()
    {
        var dataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo("C:\\DataProtectionKeys"));
        var encryptionHelper = new EncryptionHelper(dataProtectionProvider, "MyPurpose");

        var builder = new ConfigurationBuilder()
           .Add(new MyCustomConfigurationSource("your_connection_string", encryptionHelper));

        IConfigurationRoot configuration = builder.Build();

        string setting1 = configuration.GetSection("Custom:Setting1").Value;
        string setting2 = configuration.GetSection("Custom:Setting2").Value;

        Console.WriteLine($"Setting1: {setting1}, Setting2: {setting2}");
    }
}

通过这种方式,我们在自定义配置源中实现了对加密数据的处理。

2. 在扩展方法中使用解密后的配置

ConfigurationExtensionsGetSettings 方法中,假设配置对象中某些属性可能是加密的,我们可以在绑定后进行解密:

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using System;
using System.Reflection;

public static class ConfigurationExtensions
{
    public static T GetSettings<T>(this IConfiguration configuration, string sectionName, EncryptionHelper encryptionHelper) where T : new()
    {
        var section = configuration.GetSection(sectionName);
        var settings = new T();
        section.Bind(settings);

        // 对可能加密的属性进行解密
        var properties = typeof(T).GetProperties();
        foreach (var property in properties)
        {
            if (property.PropertyType == typeof(string))
            {
                string value = (string)property.GetValue(settings);
                if (value.StartsWith("ENC["))
                {
                    string decrypted = encryptionHelper.Decrypt(value.Substring(4, value.Length - 5));
                    property.SetValue(settings, decrypted);
                }
            }
        }

        return settings;
    }
}

使用示例:

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using System;

class MySettings
{
    public string EncryptedValue { get; set; }
}

class Program
{
    static void Main()
    {
        var dataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo("C:\\DataProtectionKeys"));
        var encryptionHelper = new EncryptionHelper(dataProtectionProvider, "MyPurpose");

        var builder = new ConfigurationBuilder()
           .SetBasePath(AppContext.BaseDirectory)
           .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

        IConfigurationRoot configuration = builder.Build();

        MySettings settings = configuration.GetSettings<MySettings>("MySettings", encryptionHelper);

        Console.WriteLine($"Decrypted Value: {settings.EncryptedValue}");
    }
}

通过在扩展方法中处理解密,我们可以更方便地获取和使用解密后的配置数据。

综上所述,通过对 C# 配置系统(IConfiguration)进行扩展和加密,我们可以更好地满足实际项目中对配置管理的复杂需求,提高应用程序的安全性和灵活性。无论是自定义配置源、扩展方法,还是加密配置数据,都为我们打造健壮的应用程序提供了有力的支持。在实际应用中,需要根据项目的具体情况选择合适的扩展和加密策略,确保配置数据的安全和有效管理。同时,随着应用程序的发展和需求的变化,可能需要不断调整和优化这些配置管理方案,以适应新的挑战和要求。例如,在分布式系统中,可能需要考虑如何在多个节点间同步加密密钥,或者如何更高效地管理大量的配置数据。此外,还需要关注加密算法的安全性和性能,确保加密和解密过程不会对应用程序的性能产生过大的影响。总之,配置系统的扩展与加密是一个需要综合考虑多方面因素的重要任务,对于保障应用程序的稳定运行和数据安全具有至关重要的意义。