我按照Microsoft的Hello Key Vault示例应用程序中的示例在我的ASP.Net MVC Web应用程序上设置了Azure Keyvault .
Azure KeyVault(Active Directory)AuthenticationResult默认情况下有一个小时到期 . 因此,一小时后,您必须获得一个新的身份验证令牌 . 在获得我的第一个AuthenticationResult令牌后,KeyVault正在按预期工作,但在1小时到期后,它无法获得新令牌 .
不幸的是,我的 生产环境 环境失败让我意识到这一点,因为我从未测试过去一小时的开发 .
无论如何,经过两天多的努力弄清楚我的keyvault代码出了什么问题,我提出了一个解决方案来修复我的所有问题 - 删除异步代码 - 但感觉非常hacky . 我想找出为什么它首先不起作用 .
我的代码如下所示:
public AzureEncryptionProvider() //class constructor
{
_keyVaultClient = new KeyVaultClient(GetAccessToken);
_keyBundle = _keyVaultClient
.GetKeyAsync(_keyVaultUrl, _keyVaultEncryptionKeyName)
.GetAwaiter().GetResult();
}
private static readonly string _keyVaultAuthClientId =
ConfigurationManager.AppSettings["KeyVaultAuthClientId"];
private static readonly string _keyVaultAuthClientSecret =
ConfigurationManager.AppSettings["KeyVaultAuthClientSecret"];
private static readonly string _keyVaultEncryptionKeyName =
ConfigurationManager.AppSettings["KeyVaultEncryptionKeyName"];
private static readonly string _keyVaultUrl =
ConfigurationManager.AppSettings["KeyVaultUrl"];
private readonly KeyBundle _keyBundle;
private readonly KeyVaultClient _keyVaultClient;
private static async Task<string> GetAccessToken(
string authority, string resource, string scope)
{
var clientCredential = new ClientCredential(
_keyVaultAuthClientId,
_keyVaultAuthClientSecret);
var context = new AuthenticationContext(
authority,
TokenCache.DefaultShared);
var result = context.AcquireToken(resource, clientCredential);
return result.AccessToken;
}
GetAccessToken方法签名必须是异步才能传递给新的KeyVaultClient构造函数,因此我将签名保留为async,但我删除了await关键字 .
使用await关键字(它应该是这样,并且在样本中):
private static async Task<string> GetAccessToken(string authority, string resource, string scope)
{
var clientCredential = new ClientCredential(_keyVaultAuthClientId, _keyVaultAuthClientSecret);
var context = new AuthenticationContext(authority, null);
var result = await context.AcquireTokenAsync(resource, clientCredential);
return result.AccessToken;
}
该程序在我第一次运行时工作正常 . 并且一小时,AcquireTokenAsync返回相同的原始身份验证令牌,这很棒 . 但是一旦令牌到期,AcquiteTokenAsync应该获得一个新的令牌,其新的到期日期 . 它没有 - 应用程序只是挂起 . 没有错误返回,什么都没有 .
所以调用AcquireToken而不是AcquireTokenAsync解决了这个问题,但我不明白为什么 . 你还会注意到我在我的示例代码中使用async将'null'而不是'TokenCache.DefaultShared'传递给AuthenticationContext构造函数 . 这是为了迫使toke立即过期而不是一小时后 . 否则,您必须等待一个小时才能重现该行为 .
我能够在一个全新的MVC项目中再次重现这一点,所以我认为它与我的具体项目没有任何关系 . 任何见解将不胜感激 . 但就目前而言,我只是不使用异步 .
2 回答
问题:死锁
你的
EncryptionProvider()
正在调用GetAwaiter().GetResult() . 这会阻塞线程,并在后续令牌请求中导致死锁 . 以下代码与您的代码相同,但将事物分开以便于解释 .在两个令牌请求中,执行以相同的方式开始:
AzureEncryptionProvider()
在我们称之为ThreadASP的内容中运行 .AzureEncryptionProvider()
来电GetKeyAsync()
.然后事情就不一样了 . 第一个令牌请求是多线程的:
GetKeyAsync()
返回Task .我们调用GetResult()阻止ThreadASP直到
GetKeyAsync()
完成 .GetKeyAsync()
在另一个线程上调用GetAccessToken()
.GetAccessToken()
和GetKeyAsync()
完成,释放ThreadASP .我们的网页返回给用户 . 好 .
第二个令牌请求使用单个线程:
GetKeyAsync()
在ThreadASP上调用GetAccessToken()
(不在单独的线程上 . )GetKeyAsync()
返回Task
.我们调用
GetResult()
阻止ThreadASP直到GetKeyAsync()
完成 .GetAccessToken()
必须等到ThreadASP空闲,ThreadASP必须等到GetKeyAsync()
完成,GetKeyAsync()
必须等到GetAccessToken()
完成 . 哦,哦 .死锁 .
为什么?谁知道?!?
GetKeyAsync()
中必须有一些流控制依赖于我们的访问令牌缓存的状态 . 流控制决定是否在自己的线程上运行GetAccessToken()
以及在什么时候返回Task
.解决方案:一直向下异步
为了避免死锁,这是一种最佳实践"to use async all the way down."当我们调用来自外部库的异步方法(例如
GetKeyAsync()
)时尤其如此 . 重要的是不要通过与Wait(),Result或GetResult()
同步强制该方法 . 相反,使用async and await因为await
暂停方法而不是阻塞整个线程 .异步控制器操作
异步公共方法
由于构造函数不能是异步的(因为异步方法必须返回
Task
),我们可以将异步内容放入单独的公共方法中 .谜团已揭开 . :)这是a final reference,这有助于我的理解 .
控制台应用程序
我的原始答案有这个控制台应用程序它作为初始故障排除步骤 . 它没有重现这个问题 .
控制台应用程序每五分钟循环一次,反复询问新的访问令牌 . 在每个循环中,它输出当前时间,到期时间和检索到的密钥的名称 .
在我的机器上,控制台应用程序运行了1.5小时,并在原始文件到期后成功检索到密钥 .
我有同样的挑战 . 我假设您也看过该样本发表于https://azure.microsoft.com/en-us/documentation/articles/key-vault-use-from-web-application/
该示例与我的代码所做的事情之间存在很大差异(我认为代码的意图是) . 在样本中,他们检索secrete并将其作为Utils类的静态成员存储在Web应用程序中 . 因此,该示例在应用程序的整个运行时间内检索一次秘密 .
就我而言,我在应用程序运行时的不同时间为不同目的检索不同的密钥 .
此外,您链接到的示例下载使用X.509证书来验证Web应用程序到KeyVault,而不是客户端密钥 . 这也有可能存在问题 .
我看到与@ shaun-luttin的聊天结束了你的僵局,但这不是我想的全部故事 . 我不使用.GetAwaiter() . GetResult()或从ctor调用异步方法 .