异步调用时,Azure KeyVault Active Directory AcquireTokenAsync超时

我按照Microsoft的Hello Key Vault示例应用程序中的示例在我的ASP.Net MVC Web应用程序上设置了Azure Keyvault。

Azure KeyVault(Active Directory)AuthenticationResult默认情况下有一个小时到期。 因此,一小时后,您必须获得一个新的身份validation令牌。 在获得我的第一个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 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 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返回相同的原始身份validation令牌,这很棒。 但是一旦令牌到期,AcquiteTokenAsync应该获得一个新的令牌,其新的到期日期。 它没有 – 应用程序只是挂起。 没有错误返回,什么都没有。

所以调用AcquireToken而不是AcquireTokenAsync解决了这个问题,但我不明白为什么。 你还会注意到我在我的示例代码中使用async将’null’而不是’TokenCache.DefaultShared’传递给AuthenticationContext构造函数。 这是为了迫使toke立即过期而不是一小时后。 否则,您必须等待一个小时才能重现该行为。

我能够在一个全新的MVC项目中再次重现这一点,所以我认为它与我的具体项目没有任何关系。 任何见解将不胜感激。 但就目前而言,我只是不使用异步。

问题:死锁

您的EncryptionProvider()正在调用GetAwaiter().GetResult() 。 这会阻塞线程,并在后续令牌请求中导致死锁。 以下代码与您的代码相同,但将事物分开以便于解释。

 public AzureEncryptionProvider() // runs in ThreadASP { var client = new KeyVaultClient(GetAccessToken); var task = client.GetKeyAsync(KeyVaultUrl, KeyVaultEncryptionKeyName); var awaiter = task.GetAwaiter(); // blocks ThreadASP until GetKeyAsync() completes var keyBundle = awaiter.GetResult(); } 

在两个令牌请求中,执行以相同的方式开始:

  • AzureEncryptionProvider()在我们称之为ThreadASP的内容中运行。
  • AzureEncryptionProvider()调用GetKeyAsync()

事情就不一样了。 第一个令牌请求是multithreading的:

  1. GetKeyAsync()返回一个Task
  2. 我们调用GetResult()阻止ThreadASP,直到GetKeyAsync()完成。
  3. GetKeyAsync()在另一个线程上调用GetAccessToken()
  4. GetAccessToken()GetKeyAsync()完成,释放ThreadASP。
  5. 我们的网页返回给用户。 好。

GetAccessToken在自己的线程上运行。

第二个令牌请求使用单个线程:

  1. GetKeyAsync()在ThreadASP上调用GetAccessToken() (不在单独的线程上)。
  2. GetKeyAsync()返回一个Task
  3. 我们调用GetResult()阻止ThreadASP,直到GetKeyAsync()完成。
  4. GetAccessToken()必须等到ThreadASP空闲,ThreadASP必须等到GetKeyAsync()完成, GetKeyAsync()必须等到GetAccessToken()完成。 哦,哦。
  5. 僵局。

GetAccessToken在同一个线程上运行。

为什么? 谁知道?!?

GetKeyAsync()中必须有一些流控制依赖于我们的访问令牌缓存的状态。 流控制决定是在其自己的线程上运行GetAccessToken()以及在何时返回Task

解决方案:异步完全向下

为避免死锁,最佳做法是“一直使用异步”。 当我们调用来自外部库的异步方法(例如GetKeyAsync()时尤其如此。 重要的是不要通过与Wait()ResultGetResult()同步来强制该方法。 相反,使用asyncawait因为await暂停方法而不是阻塞整个线程。

异步控制器操作

 public class HomeController : Controller { public async Task Index() { var provider = new EncryptionProvider(); await provider.GetKeyBundle(); var x = provider.MyKeyBundle; return View(); } } 

异步公共方法

由于构造函数不能是异步的(因为异步方法必须返回一个Task ),我们可以将异步内容放入一个单独的公共方法中。

 public class EncryptionProvider { // // authentication properties omitted public KeyBundle MyKeyBundle; public EncryptionProvider() { } public async Task GetKeyBundle() { var keyVaultClient = new KeyVaultClient(GetAccessToken); var keyBundleTask = await keyVaultClient .GetKeyAsync(KeyVaultUrl, KeyVaultEncryptionKeyName); MyKeyBundle = keyBundleTask; } private async Task GetAccessToken( string authority, string resource, string scope) { TokenCache.DefaultShared.Clear(); // reproduce issue var authContext = new AuthenticationContext(authority, TokenCache.DefaultShared); var clientCredential = new ClientCredential(ClientIdWeb, ClientSecretWeb); var result = await authContext.AcquireTokenAsync(resource, clientCredential); var token = result.AccessToken; return token; } } 

谜团已揭开。 :)这是一个有助于我理解的最终参考 。

控制台应用

我的原始答案有这个控制台应用程序 它作为初始故障排除步骤。 它没有重现这个问题。

控制台应用程序每五分钟循环一次,反复询问新的访问令牌。 在每个循环中,它输出当前时间,到期时间和检索到的密钥的名称。

在我的机器上,控制台应用程序运行了1.5小时,并在原始文件到期后成功检索到密钥。

 using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Azure.KeyVault; using Microsoft.IdentityModel.Clients.ActiveDirectory; namespace ConsoleApp { class Program { private static async Task RunSample() { var keyVaultClient = new KeyVaultClient(GetAccessToken); // create a key :) var keyCreate = await keyVaultClient.CreateKeyAsync( vault: _keyVaultUrl, keyName: _keyVaultEncryptionKeyName, keyType: _keyType, keyAttributes: new KeyAttributes() { Enabled = true, Expires = UnixEpoch.FromUnixTime(int.MaxValue), NotBefore = UnixEpoch.FromUnixTime(0), }, tags: new Dictionary { { "purpose", "StackOverflow Demo" } }); Console.WriteLine(string.Format( "Created {0} ", keyCreate.KeyIdentifier.Name)); // retrieve the key var keyRetrieve = await keyVaultClient.GetKeyAsync( _keyVaultUrl, _keyVaultEncryptionKeyName); Console.WriteLine(string.Format( "Retrieved {0} ", keyRetrieve.KeyIdentifier.Name)); } private static async Task GetAccessToken( string authority, string resource, string scope) { var clientCredential = new ClientCredential( _keyVaultAuthClientId, _keyVaultAuthClientSecret); var context = new AuthenticationContext( authority, TokenCache.DefaultShared); var result = await context.AcquireTokenAsync(resource, clientCredential); _expiresOn = result.ExpiresOn.DateTime; Console.WriteLine(DateTime.UtcNow.ToShortTimeString()); Console.WriteLine(_expiresOn.ToShortTimeString()); return result.AccessToken; } private static DateTime _expiresOn; private static string _keyVaultAuthClientId = "xxxxx-xxx-xxxxx-xxx-xxxxx", _keyVaultAuthClientSecret = "xxxxx-xxx-xxxxx-xxx-xxxxx", _keyVaultEncryptionKeyName = "MYENCRYPTIONKEY", _keyVaultUrl = "https://xxxxx.vault.azure.net/", _keyType = "RSA"; static void Main(string[] args) { var keepGoing = true; while (keepGoing) { RunSample().GetAwaiter().GetResult(); // sleep for five minutes System.Threading.Thread.Sleep(new TimeSpan(0, 5, 0)); if (DateTime.UtcNow > _expiresOn) { Console.WriteLine("---Expired---"); Console.ReadLine(); } } } } } 

我有同样的挑战。 我假设您还看到了样本发布在https://azure.microsoft.com/en-us/documentation/articles/key-vault-use-from-web-application/

该示例与我的代码所做的事情之间存在很大差异(我认为代码的意图是)。 在示例中,它们检索secrete并将其作为Utils类的静态成员存储在Web应用程序中。 因此,该示例在应用程序的整个运行时间内检索一次秘密。

就我而言,我在应用程序运行时的不同时间为不同目的检索不同的密钥。

此外,您链接到的示例下载使用X.509证书来validationWeb应用程序到KeyVault,而不是客户端密钥。 这也有可能存在问题。

我看到与@ shaun-luttin的聊天结论你导致了僵局,但这不是我想的全部故事。 我不使用.GetAwaiter()。GetResult()或从ctor调用异步方法。