我们使用WebJobs SDK中的[FunctionName]
属性在类中定义了一些Azure函数。该类中有多个函数,它们都需要访问存储在Azure KeyVault中的机密。问题是我们每分钟都要调用几百个函数,而且由于每个函数都在调用KeyVault,因此KeyVault失败并显示一条消息,上面写着“连接太多。通常只允许10个连接。”
@crandycodes
(Chris Anderson)建议将KeyVaultClient
设为静态。但是,我们用于KeyVaultClient
的构造函数需要构造函数的委托函数,并且不能使用静态方法作为委托。那么我们怎样才能使KeyVaultClient
静态?这应该允许函数共享客户端,减少套接字的数量。
这是我们的KeyVaultHelper
课程:
public class KeyVaultHelper
{
public string ClientId { get; protected set; }
public string ClientSecret { get; protected set; }
public string VaultUrl { get; protected set; }
public KeyVaultHelper(string clientId, string secret, string vaultName = null)
{
ClientId = clientId;
ClientSecret = secret;
VaultUrl = vaultName == null ? null : $"https://{vaultName}.vault.azure.net/";
}
public async Task<string> GetSecretAsync(string key)
{
try
{
using (var client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync),
new HttpClient()))
{
var secret = await client.GetSecretAsync(VaultUrl, key);
return secret.Value;
}
}
catch (Exception ex)
{
throw new ApplicationException($"Could not get value for secret {key}", ex);
}
}
public async Task<string> GetAccessTokenAsync(string authority, string resource, string scope)
{
var authContext = new AuthenticationContext(authority, TokenCache.DefaultShared);
var clientCred = new ClientCredential(ClientId, ClientSecret);
var result = await authContext.AcquireTokenAsync(resource, clientCred);
if (result == null)
{
throw new InvalidOperationException("Could not get token for vault");
}
return result.AccessToken;
}
}
以下是我们如何从函数中引用类:
public class ProcessorEntryPoint
{
[FunctionName("MyFuncA")]
public static async Task ProcessA(
[QueueTrigger("queue-a", Connection = "queues")]ProcessMessage msg,
TraceWriter log
)
{
var keyVaultHelper = new KeyVaultHelper(CloudConfigurationManager.GetSetting("ClientId"), CloudConfigurationManager.GetSetting("ClientSecret"),
CloudConfigurationManager.GetSetting("VaultName"));
var secret = keyVaultHelper.GetSecretAsync("mysecretkey");
// do a stuff
}
[FunctionName("MyFuncB")]
public static async Task ProcessB(
[QueueTrigger("queue-b", Connection = "queues")]ProcessMessage msg,
TraceWriter log
)
{
var keyVaultHelper = new KeyVaultHelper(CloudConfigurationManager.GetSetting("ClientId"), CloudConfigurationManager.GetSetting("ClientSecret"),
CloudConfigurationManager.GetSetting("VaultName"));
var secret = keyVaultHelper.GetSecretAsync("mysecretkey");
// do b stuff
}
}
我们可以使KeyVaultHelper
类保持静态,但反过来需要一个静态KeyVaultClient
对象来避免在每个函数调用上创建一个新连接 - 那么我们如何做到这一点还是有另一个解?我们无法相信需要KeyVault访问的功能无法扩展!?
答案 0 :(得分:4)
您可以使用内存缓存并将缓存的长度设置为您的方案中可接受的特定时间。在下面的例子中,您有一个滑动过期,您也可以使用绝对过期,具体取决于秘密何时更改。
public async Task<string> GetSecretAsync(string key)
{
MemoryCache memoryCache = MemoryCache.Default;
string mkey = VaultUrl + "_" +key;
if (!memoryCache.Contains(mkey))
{
try
{
using (var client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync),
new HttpClient()))
{
memoryCache.Add(mkey, await client.GetSecretAsync(VaultUrl, key), new CacheItemPolicy() { SlidingExpiration = TimeSpan.FromHours(1) });
}
}
catch (Exception ex)
{
throw new ApplicationException($"Could not get value for secret {key}", ex);
}
return memoryCache[mkey] as string;
}
}
答案 1 :(得分:1)
在帮助程序中尝试以下更改:
public class KeyVaultHelper
{
public string ClientId { get; protected set; }
public string ClientSecret { get; protected set; }
public string VaultUrl { get; protected set; }
KeyVaultClient client = null;
public KeyVaultHelper(string clientId, string secret, string vaultName = null)
{
ClientId = clientId;
ClientSecret = secret;
VaultUrl = vaultName == null ? null : $"https://{vaultName}.vault.azure.net/";
client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync), new HttpClient());
}
public async Task<string> GetSecretAsync(string key)
{
try
{
if (client == null)
client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync), new HttpClient());
var secret = await client.GetSecretAsync(VaultUrl, key);
return secret.Value;
}
catch (Exception ex)
{
if (client != null)
{
client.Dispose();
client = null;
}
throw new ApplicationException($"Could not get value for secret {key}", ex);
}
}
public async Task<string> GetAccessTokenAsync(string authority, string resource, string scope)
{
var authContext = new AuthenticationContext(authority, TokenCache.DefaultShared);
var clientCred = new ClientCredential(ClientId, ClientSecret);
var result = await authContext.AcquireTokenAsync(resource, clientCred);
if (result == null)
{
throw new InvalidOperationException("Could not get token for vault");
}
return result.AccessToken;
}
}
现在,该函数可以使用默认的静态构造函数来保留客户端代理:
public static class ProcessorEntryPoint
{
static KeyVaultHelper keyVaultHelper;
static ProcessorEntryPoint()
{
keyVaultHelper = new KeyVaultHelper(CloudConfigurationManager.GetSetting("ClientId"), CloudConfigurationManager.GetSetting("ClientSecret"), CloudConfigurationManager.GetSetting("VaultName"));
}
[FunctionName("MyFuncA")]
public static async Task ProcessA([QueueTrigger("queue-a", Connection = "queues")]ProcessMessage msg, TraceWriter log )
{
var secret = keyVaultHelper.GetSecretAsync("mysecretkey");
// do a stuff
}
[FunctionName("MyFuncB")]
public static async Task ProcessB([QueueTrigger("queue-b", Connection = "queues")]ProcessMessage msg, TraceWriter log )
{
var secret = keyVaultHelper.GetSecretAsync("mysecretkey");
// do b stuff
}
}
答案 2 :(得分:0)
你实际上希望KeyVault像那样扩展。它可以保护您免受不必要的成本和缓慢的行为。所有你需要做的就是保存秘密供以后使用。我已经为静态实例化创建了一个静态类。
public static class KeyVaultHelper
{
private static Dictionary<string, string> Cache = new Dictionary<string, string>();
public static async Task<string> GetSecretAsync(string secretIdentifier)
{
if (Cache.ContainsKey(secretIdentifier))
return Cache[secretIdentifier];
var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetToken));
var secretValue = (await kv.GetSecretAsync(secretIdentifier)).Value;
Cache[secretIdentifier] = secretValue;
return secretValue;
}
private static async Task<string> GetToken(string authority, string resource, string scope)
{
var clientId = ConfigurationManager.AppSettings["ClientID"];
var clientSecret = ConfigurationManager.AppSettings["ClientSecret"];
var clientCred = new ClientCredential(clientId, clientSecret);
var authContext = new AuthenticationContext(authority);
AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);
if (result == null)
throw new InvalidOperationException("Failed to obtain the JWT token");
return result.AccessToken;
}
}
现在在您的代码中,您可以执行以下操作:
private static readonly string ConnectionString = KeyVaultHelper.GetSecretAsync(ConfigurationManager.AppSettings["SqlConnectionSecretUri"]).GetAwaiter().GetResult();
现在,无论何时你需要你的秘密,它都会立即存在。
注意:如果Azure Functions由于缺乏使用而关闭实例,则静态消失并在下次调用该函数时重新加载。或者您可以使用自己的功能重新加载静态。