准备工作

DataProtectionOption

将使用数据保护需要的参数做成Option,方便配置调用

  1. public class ProtectionOption
  2. {
  3. /// <summary>应用程序名称</summary>
  4. /// <value>The name of the application.</value>
  5. public string ApplicationName { get; set; }
  6. /// <summary>证书指纹</summary>
  7. /// <value>The thumbprint.</value>
  8. public string Thumbprint { get; set; }
  9. /// <summary>私钥存储路径</summary>
  10. /// <value>The secret key path.</value>
  11. public string SecretKeyPath { get; set; }
  12. /// <summary>保护器名称</summary>
  13. /// <value>The purpose.</value>
  14. public string Purpose { get; set; }
  15. }

ProtectionOptionBase

真实项目中,不可能只对单个字符串进行加解密,大多数时候是对某个Option配置进行加解密,定义需要解密的Option需要继承的基类,Option上带有EncryptedAttribute标记的属性将会被解密

  1. /// <summary>使用数据保护的Option要继承的基类,带有EncryptedAttribute标记的将会被解密</summary>
  2. public abstract class ProtectionOptionBase
  3. {
  4. private bool Decrypted { get; set; } = false;
  5. public void Decrypt(IDataProtector protector)
  6. {
  7. if (Decrypted) return;
  8. foreach (PropertyInfo property in GetType().GetProperties())
  9. {
  10. if (property.GetCustomAttribute<EncryptedAttribute>() != null)
  11. {
  12. string text = property.GetValue(this).ToString();
  13. property.SetValue(this, protector.Unprotect(text));
  14. }
  15. }
  16. Decrypted = true;
  17. }
  18. public void CopyTo(ProtectionOptionBase option)
  19. {
  20. foreach (PropertyInfo property in GetType().GetProperties())
  21. {
  22. property.SetValue(option, property.GetValue(this));
  23. }
  24. }
  25. }

EncryptedAttribute

定义特性,用于标识属性是否需要被解密

  1. public class EncryptedAttribute : Attribute
  2. {
  3. }

DataProtectionExtensions

基于数据保护服务的扩展方法,提供注入服务并使用指定证书进行解密的能力

  1. public static class DataProtectionExtensions
  2. {
  3. /// <summary>注册数据保护服务,使用x509证书加密</summary>
  4. /// <param name="services">The services.</param>
  5. /// <returns></returns>
  6. /// <exception cref="Exception">not found X509Certificate</exception>
  7. public static IDataProtector AddDataProtectionWithX509(this IServiceCollection services)
  8. {
  9. ServiceProvider provider = services.BuildServiceProvider();
  10. // 获取用户配置数据保护的Option
  11. ProtectionOption protectionOption = provider
  12. .GetRequiredService<IOptions<ProtectionOption>>().Value;
  13. // 获取证书
  14. X509Certificate2 cert = GetCertificateFromStore(protectionOption.Thumbprint);
  15. // 注册服务
  16. services.AddDataProtection()
  17. // 设置应用程序名称
  18. .SetApplicationName(protectionOption.ApplicationName)
  19. // 设置秘钥存储路径
  20. .PersistKeysToFileSystem(new DirectoryInfo(protectionOption.SecretKeyPath))
  21. // 设置用于加密的证书
  22. .UnprotectKeysWithAnyCertificate(cert);
  23. provider = services.BuildServiceProvider();
  24. IDataProtectionProvider protectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
  25. // 创建数据保护器
  26. IDataProtector protector = protectionProvider.CreateProtector(protectionOption.Purpose);
  27. // 注册单例的数据保护器
  28. services.AddSingleton<IDataProtector>(serviceProvider => protector);
  29. return protector;
  30. }
  31. public static IServiceCollection ProtectedConfigure<TOptions>(this IServiceCollection services
  32. , IConfigurationSection section)
  33. where TOptions : ProtectionOptionBase, new()
  34. {
  35. ServiceProvider provider = services.BuildServiceProvider();
  36. IDataProtector protector = provider.GetRequiredService<IDataProtector>();
  37. services.Configure<TOptions>(option =>
  38. {
  39. var config = section.Get<TOptions>();
  40. config.Decrypt(protector);
  41. config.CopyTo(option);
  42. });
  43. return services;
  44. }
  45. /// <summary>
  46. /// Get the certifcate to use to encrypt the key
  47. /// CertSearchArea:StoreLocation.CurrentUser/StoreLocation.LocalMachine
  48. /// </summary>
  49. /// <param name="thumbprint"></param>
  50. /// <returns></returns>
  51. public static X509Certificate2 GetCertificateFromStore(string thumbprint)
  52. {
  53. X509Certificate2 signingCert =
  54. GetCertificateFromStore(thumbprint, StoreLocation.CurrentUser);
  55. if (signingCert != null)
  56. {
  57. return signingCert;
  58. }
  59. else
  60. {
  61. signingCert =
  62. GetCertificateFromStore(thumbprint, StoreLocation.LocalMachine);
  63. if (signingCert != null)
  64. {
  65. return signingCert;
  66. }
  67. }
  68. throw new X509Certificate2Exception("本机和当前用户证书存储区都未找到对应证书");
  69. }
  70. /// <summary>Gets the certificate from store.</summary>
  71. /// <param name="thumbprint">The thumbprint.</param>
  72. /// <param name="storeLocation">The store location.</param>
  73. /// <returns></returns>
  74. public static X509Certificate2 GetCertificateFromStore(string thumbprint, StoreLocation storeLocation)
  75. {
  76. // 获取本地机器证书存储
  77. X509Store localMachineStore = new X509Store(storeLocation);
  78. try
  79. {
  80. localMachineStore.Open(OpenFlags.ReadOnly);
  81. X509Certificate2Collection certCollection = localMachineStore.Certificates;
  82. X509Certificate2Collection localMachineCerts =
  83. certCollection.Find(X509FindType.FindByTimeValid, DateTime.Now, false);
  84. X509Certificate2Collection signingCert =
  85. localMachineCerts.Find(X509FindType.FindByThumbprint, thumbprint, false);
  86. if (signingCert.Count == 0)
  87. {
  88. return null;
  89. }
  90. return signingCert[0];
  91. }
  92. finally
  93. {
  94. localMachineStore.Close();
  95. }
  96. }
  97. }

GeneratingCiphertext.Console

依赖指定证书生成加密文件secret.xml以及对应{key}.xml文件;{key}.xml是生成秘钥文件,secret.xml在之后的管道集成中会使用到

image.png

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

image.png

示例:

  1. --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的配置;

  1. {
  2. // 数据保护相关配置;SecretKeyPath:固定目录
  3. "ProtectionOption": {
  4. "Thumbprint": "CD Replace",
  5. "ApplicationName": "CD Replace",
  6. "SecretKeyPath": "DataProtection\\app-keys",
  7. "Purpose": "CD Replace"
  8. }
  9. }

appsettings.Development.json

appsettings.Development.json只包含针对当前环境的配置,直接明文存储

  1. {
  2. // 开发环境:明文存储
  3. "TestOption": {
  4. "Test1": "dev-test1",
  5. "Test2": "dev-test2"
  6. },
  7. "DataBase": {
  8. "DbConnection": "Server=.;Database=WebApp01.Database;uid=sa;pwd=wpl19950815;"
  9. }
  10. }

appsettings.Sit.json

appsettings.Sit.json只包含针对当前环境的配置;TestOption.Test2添加了Encrypted标记用于测试解密Option;DataBase.DbConnection用于测试解密字符串

  1. namespace WebApp01.Options
  2. {
  3. using WebApp01.CustomDataProtection;
  4. public class TestOption : ProtectionOptionBase
  5. {
  6. public string Test1 { get; set; }
  7. [Encrypted]
  8. public string Test2 { get; set; }
  9. }
  10. }
  1. {
  2. // 数据保护相关配置;SecretKeyPath:固定目录
  3. "ProtectionOption": {
  4. "Thumbprint": "CD Replace",
  5. "ApplicationName": "CD Replace",
  6. "SecretKeyPath": "DataProtection\\app-keys",
  7. "Purpose": "CD Replace"
  8. },
  9. // 测试Option解密
  10. "TestOption": {
  11. "Test1": "sit-test1",
  12. "Test2": "CD Replace"
  13. },
  14. // Sit环境:需要加密的配置在CD时进行替换
  15. "DataBase": {
  16. "DbConnection": "CD Replace"
  17. }
  18. }

Startup

在Startup中使用服务

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. if (Environment.IsDevelopment())
  4. {
  5. services.Configure<TestOption>(Configuration.GetSection("TestOption"));
  6. }
  7. else
  8. {
  9. // 注入数据保护需要的Option
  10. services.Configure<ProtectionOption>(Configuration.GetSection("ProtectionOption"));
  11. // 注入数据保护服务(依赖指定证书)
  12. IDataProtector dataProtector = services.AddDataProtectionWithX509();
  13. // 解密字符串
  14. string connStr = dataProtector.Unprotect(Configuration.GetSection("Database:ConnectString").Value);
  15. Console.WriteLine(connStr);
  16. // 解密Option;Option上带有EncryptedAttribute标记的属性将会被解密
  17. services.ProtectedConfigure<TestOption>(Configuration.GetSection("TestOption"));
  18. }
  19. services.AddControllers();
  20. }

EnvironmentController

定义测试接口

  1. [ApiController]
  2. [Route("[controller]")]
  3. public class EnvironmentController : ControllerBase
  4. {
  5. private readonly IOptions<TestOption> testOption;
  6. public EnvironmentController(IOptions<TestOption> testOption)
  7. {
  8. this.testOption = testOption;
  9. }
  10. [HttpGet]
  11. public IActionResult GetEnvironmentVariables()
  12. {
  13. Dictionary<string, string> dicts = new Dictionary<string, string>();
  14. ConfigurationBuilder builder = new ConfigurationBuilder();
  15. builder.AddJsonFile("appsettings.json");
  16. IConfigurationRoot configuration = builder.Build();
  17. dicts.Add("ProtectionOption.Thumbprint", configuration.GetSection("ProtectionOption:Thumbprint").Value);
  18. dicts.Add("ProtectionOption.ApplicationName", configuration.GetSection("ProtectionOption:ApplicationName").Value);
  19. dicts.Add("ProtectionOption.SecretKeyPath", configuration.GetSection("ProtectionOption:SecretKeyPath").Value);
  20. dicts.Add("ProtectionOption.Purpose", configuration.GetSection("ProtectionOption:Purpose").Value);
  21. dicts.Add("TestOption.Test1", testOption.Value.Test1);
  22. dicts.Add("TestOption.Test2", testOption.Value.Test2);
  23. dicts.Add("DataBase.DbConnection", configuration.GetSection("DataBase:DbConnection").Value);
  24. return Ok(JsonConvert.SerializeObject(dicts));
  25. }
  26. }