准备工作
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();
// 获取用户配置数据保护的Option
ProtectionOption 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
{
// 注入数据保护需要的Option
services.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));
}
}