ASP.NET 核心中的加密配置

随着 web.config的消失,在使用 ASP.NET Core 构建的 Web 应用程序的配置中存储敏感信息(密码、令牌)的首选方法是什么?

appsettings.json中有没有自动获得加密配置部分的方法?

83795 次浏览

用户机密看起来像是一个存储密码的很好的解决方案,一般来说,还有应用程序机密,至少是 在开发过程中

检查 Microsoft 官方文档。你也可以检查 这个其他 SO 问题。

这只是在开发过程中“隐藏”您的秘密的一种方法,并且可以避免将它们泄露到源代码树中; Secret Manager 工具 不加密储存的秘密不应该被视为可信存储。

如果希望将加密的 appsettings.json投入生产,可以通过构建 自定义配置提供程序来实现。

例如:

public class CustomConfigProvider : ConfigurationProvider, IConfigurationSource
{
public CustomConfigProvider()
{
}


public override void Load()
{
Data = UnencryptMyConfiguration();
}


private IDictionary<string, string> UnencryptMyConfiguration()
{
// do whatever you need to do here, for example load the file and unencrypt key by key
//Like:
var configValues = new Dictionary<string, string>
{
{"key1", "unencryptedValue1"},
{"key2", "unencryptedValue2"}
};
return configValues;
}


private IDictionary<string, string> CreateAndSaveDefaultValues(IDictionary<string, string> defaultDictionary)
{
var configValues = new Dictionary<string, string>
{
{"key1", "encryptedValue1"},
{"key2", "encryptedValue2"}
};
return configValues;
}


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

为扩展方法定义一个静态类:

public static class CustomConfigProviderExtensions
{
public static IConfigurationBuilder AddEncryptedProvider(this IConfigurationBuilder builder)
{
return builder.Add(new CustomConfigProvider());
}
}

然后你可以激活它:

// Set up configuration sources.
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEncryptedProvider()
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

我不想写一个自定义提供程序-太多的工作。我只是想进入 JsonConfigurationProvider,所以我想出了一个适合我的方法,希望它能帮到别人。

public class JsonConfigurationProvider2 : JsonConfigurationProvider
{
public JsonConfigurationProvider2(JsonConfigurationSource2 source) : base(source)
{
}


public override void Load(Stream stream)
{
// Let the base class do the heavy lifting.
base.Load(stream);


// Do decryption here, you can tap into the Data property like so:


Data["abc:password"] = MyEncryptionLibrary.Decrypt(Data["abc:password"]);


// But you have to make your own MyEncryptionLibrary, not included here
}
}


public class JsonConfigurationSource2 : JsonConfigurationSource
{
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new JsonConfigurationProvider2(this);
}
}


public static class JsonConfigurationExtensions2
{
public static IConfigurationBuilder AddJsonFile2(this IConfigurationBuilder builder, string path, bool optional,
bool reloadOnChange)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("File path must be a non-empty string.");
}


var source = new JsonConfigurationSource2
{
FileProvider = null,
Path = path,
Optional = optional,
ReloadOnChange = reloadOnChange
};


source.ResolveFileProvider();
builder.Add(source);
return builder;
}
}

我同意@CoderSteve 的观点,即编写一个全新的提供程序是一项繁重的工作。它也没有建立在现有的标准 JSON 架构之上。下面是我在标准 JSON 体系结构之上构建的解决方案,使用首选的。NetCore 加密库,并且对 DI 非常友好。

public static class IServiceCollectionExtensions
{
public static IServiceCollection AddProtectedConfiguration(this IServiceCollection services)
{
services
.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"c:\keys"))
.ProtectKeysWithDpapi();


return services;
}


public static IServiceCollection ConfigureProtected<TOptions>(this IServiceCollection services, IConfigurationSection section) where TOptions: class, new()
{
return services.AddSingleton(provider =>
{
var dataProtectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
section = new ProtectedConfigurationSection(dataProtectionProvider, section);


var options = section.Get<TOptions>();
return Options.Create(options);
});
}


private class ProtectedConfigurationSection : IConfigurationSection
{
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly IConfigurationSection _section;
private readonly Lazy<IDataProtector> _protector;


public ProtectedConfigurationSection(
IDataProtectionProvider dataProtectionProvider,
IConfigurationSection section)
{
_dataProtectionProvider = dataProtectionProvider;
_section = section;


_protector = new Lazy<IDataProtector>(() => dataProtectionProvider.CreateProtector(section.Path));
}


public IConfigurationSection GetSection(string key)
{
return new ProtectedConfigurationSection(_dataProtectionProvider, _section.GetSection(key));
}


public IEnumerable<IConfigurationSection> GetChildren()
{
return _section.GetChildren()
.Select(x => new ProtectedConfigurationSection(_dataProtectionProvider, x));
}


public IChangeToken GetReloadToken()
{
return _section.GetReloadToken();
}


public string this[string key]
{
get => GetProtectedValue(_section[key]);
set => _section[key] = _protector.Value.Protect(value);
}


public string Key => _section.Key;
public string Path => _section.Path;


public string Value
{
get => GetProtectedValue(_section.Value);
set => _section.Value = _protector.Value.Protect(value);
}


private string GetProtectedValue(string value)
{
if (value == null)
return null;


return _protector.Value.Unprotect(value);
}
}
}

像下面这样连接受保护的配置部分:

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();


// Configure normal config settings
services.Configure<MySettings>(Configuration.GetSection("MySettings"));


// Configure protected config settings
services.AddProtectedConfiguration();
services.ConfigureProtected<MyProtectedSettings>(Configuration.GetSection("MyProtectedSettings"));
}

您可以使用下面这样的控制器轻松地为配置文件创建加密值:

[Route("encrypt"), HttpGet, HttpPost]
public string Encrypt(string section, string value)
{
var protector = _dataProtectionProvider.CreateProtector(section);
return protector.Protect(value);
}

用法: http://localhost/cryptography/encrypt?section=SectionName:KeyName&value=PlainTextValue

public static IServiceCollection ConfigureProtected<TOptions>(this IServiceCollection services, IConfigurationSection section) where TOptions: class, new()
{
return services.AddSingleton(provider =>
{
var dataProtectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
var protectedSection = new ProtectedConfigurationSection(dataProtectionProvider, section);


var options = protectedSection.Get<TOptions>();
return Options.Create(options);
});
}

这个方法是正确的

只是一些澄清,以帮助避免问题。当你加密一个值时,它使用的部分是“目的”(https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/purpose-strings?view=aspnetcore-2.2)当你得到一个“有效载荷无效”或类似的东西时,很可能你用来加密它的目的,与用来解密它的目的不同。因此,假设我在 appsetings.json 中有一个名为‘ SecureSettings’的第一级节,其中包含一个连接字符串:

{
"SecureSettings":
{
"ConnectionString":"MyClearTextConnectionString"
}
}

要对值进行加密,我会调用: http://localhost/cryptography/encrypt?section=SecureSettings:ConnectionString&value=MyClearTextConnectionString

顺便说一下,你可能不想在应用程序本身中保留一个加密控制器。

我设法创建了一个定制的 JSON 配置提供程序,它使用 DPAPI 来加密和解密机密。它基本上使用您可以定义的简单正则表达式来指定 JSON 的哪些部分需要加密。

执行下列步骤:

  1. Json 文件已加载
  2. 确定匹配给定正则表达式的 JSON 部分是否已经加密(或未加密)。这是通过对 JSON 部分的 base-64解码来完成的,并验证它是否以预期的前缀 ! ENC!开始)
  3. 如果未加密,则首先使用 DPAPI 对 JSON 部分进行加密,然后添加前缀 ! ENC!并对 base-64进行编码
  4. 用 JSON 文件中的加密(基于64)值覆盖未加密的 JSON 部分

请注意,base-64并没有带来更好的安全性,只是出于表面上的原因而隐藏了前缀 ! ENC!。这当然只是品味的问题;)

这个解决方案由以下类组成:

  1. ProtectedJsonConfigurationProvider class (= 自定义 JsonConfigurationProvider)
  2. ProtectedJsonConfigurationSource class (= customJsonConfigurationSource)
  3. 为了简单地添加受保护的配置,IConfigurationBuilder上的 AddProtectedJsonFile () 扩展方法

假设有以下初始 鉴定 Json文件:

{
"authentication": {
"credentials": [
{
user: "john",
password: "just a password"
},
{
user: "jane",
password: "just a password"
}
]
}
}

它在加载后变成(某种程度上)如下所示

{
"authentication": {
"credentials": [
{
"user": "john",
"password": "IUVOQyEBAAAA0Iyd3wEV0R=="
},
{
"user": "jane",
"password": "IUVOQyEBAAAA0Iyd3wEV0R=="
}
]
}
}

并假设基于 json 格式的配置类如下

public class AuthenticationConfiguration
{
[JsonProperty("credentials")]
public Collection<CredentialConfiguration> Credentials { get; set; }
}


public class CredentialConfiguration
{
[JsonProperty("user")]
public string User { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}

示例代码如下:

//Note that the regular expression will cause the authentication.credentials.password path to be encrypted.
//Also note that the byte[] contains the entropy to increase security
var configurationBuilder = new ConfigurationBuilder()
.AddProtectedJsonFile("authentication.json", true, new byte[] { 9, 4, 5, 6, 2, 8, 1 },
new Regex("authentication:credentials:[0-9]*:password"));


var configuration = configurationBuilder.Build();
var authenticationConfiguration = configuration.GetSection("authentication").Get<AuthenticationConfiguration>();


//Get the decrypted password from the encrypted JSON file.
//Note that the ProtectedJsonConfigurationProvider.TryGet() method is called (I didn't expect that :D!)
var password = authenticationConfiguration.Credentials.First().Password

安装 Microsoft.Extensions.Configuration.Binder 包,以获得 configation.GetSection (“身份验证”) .获取 < T > ()实现

最后是奇迹发生的类:)

/// <summary>Represents a <see cref="ProtectedJsonConfigurationProvider"/> source</summary>
public class ProtectedJsonConfigurationSource : JsonConfigurationSource
{
/// <summary>Gets the byte array to increse protection</summary>
internal byte[] Entropy { get; private set; }


/// <summary>Represents a <see cref="ProtectedJsonConfigurationProvider"/> source</summary>
/// <param name="entropy">Byte array to increase protection</param>
/// <exception cref="ArgumentNullException"/>
public ProtectedJsonConfigurationSource(byte[] entropy)
{
this.Entropy = entropy ?? throw new ArgumentNullException(Localization.EntropyNotSpecifiedError);
}


/// <summary>Builds the configuration provider</summary>
/// <param name="builder">Builder to build in</param>
/// <returns>Returns the configuration provider</returns>
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new ProtectedJsonConfigurationProvider(this);
}


/// <summary>Gets or sets the protection scope of the configuration provider. Default value is <see cref="DataProtectionScope.CurrentUser"/></summary>
public DataProtectionScope Scope { get; set; }
/// <summary>Gets or sets the regular expressions that must match the keys to encrypt</summary>
public IEnumerable<Regex> EncryptedKeyExpressions { get; set; }
}


/// <summary>Represents a provider that protects a JSON configuration file</summary>
public partial class ProtectedJsonConfigurationProvider : JsonConfigurationProvider
{
private readonly ProtectedJsonConfigurationSource protectedSource;
private readonly HashSet<string> encryptedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private static readonly byte[] encryptedPrefixBytes = Encoding.UTF8.GetBytes("!ENC!");


/// <summary>Checks whether the given text is encrypted</summary>
/// <param name="text">Text to check</param>
/// <returns>Returns true in case the text is encrypted</returns>
private bool isEncrypted(string text)
{
if (text == null) { return false; }


//Decode the data in order to verify whether the decoded data starts with the expected prefix
byte[] decodedBytes;
try { decodedBytes = Convert.FromBase64String(text); }
catch (FormatException) { return false; }


return decodedBytes.Length >= encryptedPrefixBytes.Length
&& decodedBytes.AsSpan(0, encryptedPrefixBytes.Length).SequenceEqual(encryptedPrefixBytes);
}


/// <summary>Converts the given key to the JSON token path equivalent</summary>
/// <param name="key">Key to convert</param>
/// <returns>Returns the JSON token path equivalent</returns>
private string convertToTokenPath(string key)
{
var jsonStringBuilder = new StringBuilder();


//Split the key by ':'
var keyParts = key.Split(':');
for (var keyPartIndex = 0; keyPartIndex < keyParts.Length; keyPartIndex++)
{
var keyPart = keyParts[keyPartIndex];


if (keyPart.All(char.IsDigit)) { jsonStringBuilder.Append('[').Append(keyPart).Append(']'); }
else if (keyPartIndex > 0) { jsonStringBuilder.Append('.').Append(keyPart); }
else { jsonStringBuilder.Append(keyPart); }
}


return jsonStringBuilder.ToString();
}


/// <summary>Writes the given encrypted key/values to the JSON oconfiguration file</summary>
/// <param name="encryptedKeyValues">Encrypted key/values to write</param>
private void writeValues(IDictionary<string, string> encryptedKeyValues)
{
try
{
if (encryptedKeyValues == null || encryptedKeyValues.Count == 0) { return; }


using (var stream = new FileStream(this.protectedSource.Path, FileMode.Open, FileAccess.ReadWrite))
{
JObject json;


using (var streamReader = new StreamReader(stream, Encoding.UTF8, true, 4096, true))
{
using (var jsonTextReader = new JsonTextReader(streamReader))
{
json = JObject.Load(jsonTextReader);


foreach (var encryptedKeyValue in encryptedKeyValues)
{
var tokenPath = this.convertToTokenPath(encryptedKeyValue.Key);
var value = json.SelectToken(tokenPath) as JValue;
if (value.Value != null) { value.Value = encryptedKeyValue.Value; }
}
}
}


stream.Seek(0, SeekOrigin.Begin);
using (var streamWriter = new StreamWriter(stream))
{
using (var jsonTextWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented })
{
json.WriteTo(jsonTextWriter);
}
}
}
}
catch (Exception exception)
{
throw new Exception(string.Format(Localization.ProtectedJsonConfigurationWriteEncryptedValues, this.protectedSource.Path), exception);
}
}


/// <summary>Represents a provider that protects a JSON configuration file</summary>
/// <param name="source">Settings of the source</param>
/// <see cref="ArgumentNullException"/>
public ProtectedJsonConfigurationProvider(ProtectedJsonConfigurationSource source) : base(source)
{
this.protectedSource = source as ProtectedJsonConfigurationSource;
}


/// <summary>Loads the JSON data from the given <see cref="Stream"/></summary>
/// <param name="stream"><see cref="Stream"/> to load</param>
public override void Load(Stream stream)
{
//Call the base method first to ensure the data to be available
base.Load(stream);


var expressions = protectedSource.EncryptedKeyExpressions;
if (expressions != null)
{
//Dictionary that contains the keys (and their encrypted value) that must be written to the JSON file
var encryptedKeyValuesToWrite = new Dictionary<string, string>();


//Iterate through the data in order to verify whether the keys that require to be encrypted, as indeed encrypted.
//Copy the keys to a new string array in order to avoid a collection modified exception
var keys = new string[this.Data.Keys.Count];
this.Data.Keys.CopyTo(keys, 0);


foreach (var key in keys)
{
//Iterate through each expression in order to check whether the current key must be encrypted and is encrypted.
//If not then encrypt the value and overwrite the key
var value = this.Data[key];
if (!string.IsNullOrEmpty(value) && expressions.Any(e => e.IsMatch(key)))
{
this.encryptedKeys.Add(key);


//Verify whether the value is encrypted
if (!this.isEncrypted(value))
{
var protectedValue = ProtectedData.Protect(Encoding.UTF8.GetBytes(value), protectedSource.Entropy, protectedSource.Scope);
var protectedValueWithPrefix = new List<byte>(encryptedPrefixBytes);
protectedValueWithPrefix.AddRange(protectedValue);


//Convert the protected value to a base-64 string in order to mask the prefix (for cosmetic purposes)
//and overwrite the key with the encrypted value
var protectedBase64Value = Convert.ToBase64String(protectedValueWithPrefix.ToArray());
encryptedKeyValuesToWrite.Add(key, protectedBase64Value);
this.Data[key] = protectedBase64Value;
}
}
}


//Write the encrypted key/values to the JSON configuration file
this.writeValues(encryptedKeyValuesToWrite);
}
}


/// <summary>Attempts to get the value of the given key</summary>
/// <param name="key">Key to get</param>
/// <param name="value">Value of the key</param>
/// <returns>Returns true in case the key has been found</returns>
public override bool TryGet(string key, out string value)
{
if (!base.TryGet(key, out value)) { return false; }
else if (!this.encryptedKeys.Contains(key)) { return true; }


//Key is encrypted and must therefore be decrypted in order to return.
//Note that the decoded base-64 bytes contains the encrypted prefix which must be excluded when unprotection
var protectedValueWithPrefix = Convert.FromBase64String(value);
var protectedValue = new byte[protectedValueWithPrefix.Length - encryptedPrefixBytes.Length];
Buffer.BlockCopy(protectedValueWithPrefix, encryptedPrefixBytes.Length, protectedValue, 0, protectedValue.Length);


var unprotectedValue = ProtectedData.Unprotect(protectedValue, this.protectedSource.Entropy, this.protectedSource.Scope);
value = Encoding.UTF8.GetString(unprotectedValue);
return true;
}


/// <summary>Provides extensions concerning <see cref="ProtectedJsonConfigurationProvider"/></summary>
public static class ProtectedJsonConfigurationProviderExtensions
{
/// <summary>Adds a protected JSON file</summary>
/// <param name="configurationBuilder"><see cref="IConfigurationBuilder"/> in which to apply the JSON file</param>
/// <param name="path">Path to the JSON file</param>
/// <param name="optional">Specifies whether the JSON file is optional</param>
/// <param name="entropy">Byte array to increase protection</param>
/// <returns>Returns the <see cref="IConfigurationBuilder"/></returns>
/// <exception cref="ArgumentNullException"/>
public static IConfigurationBuilder AddProtectedJsonFile(this IConfigurationBuilder configurationBuilder, string path, bool optional, byte[] entropy, params Regex[] encryptedKeyExpressions)
{
var source = new ProtectedJsonConfigurationSource(entropy)
{
Path = path,
Optional = optional,
EncryptedKeyExpressions = encryptedKeyExpressions
};


return configurationBuilder.Add(source);
}
}