准备工作
DataProtectionOption
将使用数据保护需要的参数做成Option,方便配置调用
public class ProtectionOption{/// <summary>应用程序名称</summary>/// <value>The name of the application.</value>public string ApplicationName { get; set; }/// <summary>证书指纹</summary>/// <value>The thumbprint.</value>public string Thumbprint { get; set; }/// <summary>私钥存储路径</summary>/// <value>The secret key path.</value>public string SecretKeyPath { get; set; }/// <summary>保护器名称</summary>/// <value>The purpose.</value>public string Purpose { get; set; }}
ProtectionOptionBase
真实项目中,不可能只对单个字符串进行加解密,大多数时候是对某个Option配置进行加解密,定义需要解密的Option需要继承的基类,Option上带有
EncryptedAttribute标记的属性将会被解密
/// <summary>使用数据保护的Option要继承的基类,带有EncryptedAttribute标记的将会被解密</summary>public abstract class ProtectionOptionBase{private bool Decrypted { get; set; } = false;public void Decrypt(IDataProtector protector){if (Decrypted) return;foreach (PropertyInfo property in GetType().GetProperties()){if (property.GetCustomAttribute<EncryptedAttribute>() != null){string text = property.GetValue(this).ToString();property.SetValue(this, protector.Unprotect(text));}}Decrypted = true;}public void CopyTo(ProtectionOptionBase option){foreach (PropertyInfo property in GetType().GetProperties()){property.SetValue(option, property.GetValue(this));}}}
EncryptedAttribute
定义特性,用于标识属性是否需要被解密
public class EncryptedAttribute : Attribute{}
DataProtectionExtensions
基于数据保护服务的扩展方法,提供注入服务并使用指定证书进行解密的能力
public static class DataProtectionExtensions{/// <summary>注册数据保护服务,使用x509证书加密</summary>/// <param name="services">The services.</param>/// <returns></returns>/// <exception cref="Exception">not found X509Certificate</exception>public static IDataProtector AddDataProtectionWithX509(this IServiceCollection services){ServiceProvider provider = services.BuildServiceProvider();// 获取用户配置数据保护的OptionProtectionOption protectionOption = provider.GetRequiredService<IOptions<ProtectionOption>>().Value;// 获取证书X509Certificate2 cert = GetCertificateFromStore(protectionOption.Thumbprint);// 注册服务services.AddDataProtection()// 设置应用程序名称.SetApplicationName(protectionOption.ApplicationName)// 设置秘钥存储路径.PersistKeysToFileSystem(new DirectoryInfo(protectionOption.SecretKeyPath))// 设置用于加密的证书.UnprotectKeysWithAnyCertificate(cert);provider = services.BuildServiceProvider();IDataProtectionProvider protectionProvider = provider.GetRequiredService<IDataProtectionProvider>();// 创建数据保护器IDataProtector protector = protectionProvider.CreateProtector(protectionOption.Purpose);// 注册单例的数据保护器services.AddSingleton<IDataProtector>(serviceProvider => protector);return protector;}public static IServiceCollection ProtectedConfigure<TOptions>(this IServiceCollection services, IConfigurationSection section)where TOptions : ProtectionOptionBase, new(){ServiceProvider provider = services.BuildServiceProvider();IDataProtector protector = provider.GetRequiredService<IDataProtector>();services.Configure<TOptions>(option =>{var config = section.Get<TOptions>();config.Decrypt(protector);config.CopyTo(option);});return services;}/// <summary>/// Get the certifcate to use to encrypt the key/// CertSearchArea:StoreLocation.CurrentUser/StoreLocation.LocalMachine/// </summary>/// <param name="thumbprint"></param>/// <returns></returns>public static X509Certificate2 GetCertificateFromStore(string thumbprint){X509Certificate2 signingCert =GetCertificateFromStore(thumbprint, StoreLocation.CurrentUser);if (signingCert != null){return signingCert;}else{signingCert =GetCertificateFromStore(thumbprint, StoreLocation.LocalMachine);if (signingCert != null){return signingCert;}}throw new X509Certificate2Exception("本机和当前用户证书存储区都未找到对应证书");}/// <summary>Gets the certificate from store.</summary>/// <param name="thumbprint">The thumbprint.</param>/// <param name="storeLocation">The store location.</param>/// <returns></returns>public static X509Certificate2 GetCertificateFromStore(string thumbprint, StoreLocation storeLocation){// 获取本地机器证书存储X509Store localMachineStore = new X509Store(storeLocation);try{localMachineStore.Open(OpenFlags.ReadOnly);X509Certificate2Collection certCollection = localMachineStore.Certificates;X509Certificate2Collection localMachineCerts =certCollection.Find(X509FindType.FindByTimeValid, DateTime.Now, false);X509Certificate2Collection signingCert =localMachineCerts.Find(X509FindType.FindByThumbprint, thumbprint, false);if (signingCert.Count == 0){return null;}return signingCert[0];}finally{localMachineStore.Close();}}}
GeneratingCiphertext.Console
依赖指定证书生成加密文件
secret.xml以及对应{key}.xml文件;{key}.xml是生成秘钥文件,secret.xml在之后的管道集成中会使用到

注意:在启动时必须提供必需的参数才可生成
secret.xml及{key}.xml文件;在管道中使用命令行操作,在Visual Studio中可以使用如下方式设置

示例:
--cpath="C:\\Users\\WangPengLiang\\Desktop\\DataProtection\\dev.pfx" --cpass="wpl19950815" --purpose="RedPI.Todo" --spath="C:\\Users\\WangPengLiang\\Desktop\\DataProtection\\app-keys" --appname="RedPI.Todo" --snodes="a:1, b:2" --soutputpath="C:\Users\WangPengLiang\Desktop\DataProtection"
项目应用
appsettings.json
appsettings.json中包含了通用的Option的配置;
{// 数据保护相关配置;SecretKeyPath:固定目录"ProtectionOption": {"Thumbprint": "CD Replace","ApplicationName": "CD Replace","SecretKeyPath": "DataProtection\\app-keys","Purpose": "CD Replace"}}
appsettings.Development.json
appsettings.Development.json只包含针对当前环境的配置,直接明文存储
{// 开发环境:明文存储"TestOption": {"Test1": "dev-test1","Test2": "dev-test2"},"DataBase": {"DbConnection": "Server=.;Database=WebApp01.Database;uid=sa;pwd=wpl19950815;"}}
appsettings.Sit.json
appsettings.Sit.json只包含针对当前环境的配置;TestOption.Test2添加了Encrypted标记用于测试解密Option;DataBase.DbConnection用于测试解密字符串
namespace WebApp01.Options{using WebApp01.CustomDataProtection;public class TestOption : ProtectionOptionBase{public string Test1 { get; set; }[Encrypted]public string Test2 { get; set; }}}
{// 数据保护相关配置;SecretKeyPath:固定目录"ProtectionOption": {"Thumbprint": "CD Replace","ApplicationName": "CD Replace","SecretKeyPath": "DataProtection\\app-keys","Purpose": "CD Replace"},// 测试Option解密"TestOption": {"Test1": "sit-test1","Test2": "CD Replace"},// Sit环境:需要加密的配置在CD时进行替换"DataBase": {"DbConnection": "CD Replace"}}
Startup
在Startup中使用服务
public void ConfigureServices(IServiceCollection services){if (Environment.IsDevelopment()){services.Configure<TestOption>(Configuration.GetSection("TestOption"));}else{// 注入数据保护需要的Optionservices.Configure<ProtectionOption>(Configuration.GetSection("ProtectionOption"));// 注入数据保护服务(依赖指定证书)IDataProtector dataProtector = services.AddDataProtectionWithX509();// 解密字符串string connStr = dataProtector.Unprotect(Configuration.GetSection("Database:ConnectString").Value);Console.WriteLine(connStr);// 解密Option;Option上带有EncryptedAttribute标记的属性将会被解密services.ProtectedConfigure<TestOption>(Configuration.GetSection("TestOption"));}services.AddControllers();}
EnvironmentController
定义测试接口
[ApiController][Route("[controller]")]public class EnvironmentController : ControllerBase{private readonly IOptions<TestOption> testOption;public EnvironmentController(IOptions<TestOption> testOption){this.testOption = testOption;}[HttpGet]public IActionResult GetEnvironmentVariables(){Dictionary<string, string> dicts = new Dictionary<string, string>();ConfigurationBuilder builder = new ConfigurationBuilder();builder.AddJsonFile("appsettings.json");IConfigurationRoot configuration = builder.Build();dicts.Add("ProtectionOption.Thumbprint", configuration.GetSection("ProtectionOption:Thumbprint").Value);dicts.Add("ProtectionOption.ApplicationName", configuration.GetSection("ProtectionOption:ApplicationName").Value);dicts.Add("ProtectionOption.SecretKeyPath", configuration.GetSection("ProtectionOption:SecretKeyPath").Value);dicts.Add("ProtectionOption.Purpose", configuration.GetSection("ProtectionOption:Purpose").Value);dicts.Add("TestOption.Test1", testOption.Value.Test1);dicts.Add("TestOption.Test2", testOption.Value.Test2);dicts.Add("DataBase.DbConnection", configuration.GetSection("DataBase:DbConnection").Value);return Ok(JsonConvert.SerializeObject(dicts));}}
