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

时间:2015-09-15 20:02:09

标签: c# asp.net azure async-await azure-active-directory

我按照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解决了这个问题,但我不明白为什么。你还会注意到我已经过了“无效”#39;代替&#39; TokenCache.DefaultShared&#39;使用async进入示例代码中的AuthenticationContext构造函数。这是为了迫使toke立即过期而不是一小时后过期。否则,您必须等待一个小时才能重现该行为。

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

2 个答案:

答案 0 :(得分:23)

问题:死锁

您的<section class="products"> <h1>Fans</h1> <p> <ul class="fans"> <li> <a href="#"> <figure> <img class="fans" src="http://placehold.it/300/300" alt="Shrouded"/> <figcaption>Shrouded</figcaption> </figure> </a> </li> <li> <a href="#"> <figure> <img class="fans" src="http://placehold.it/300/300" alt="Shrouded" /> <figcaption>Shallow Recess</figcaption> </figure> </a> </li> </ul> </p> </section> 正在呼叫GetAwaiter().GetResult()。这会阻塞线程,并在后续令牌请求中导致死锁。以下代码与您的代码相同,但将事物分开以便于解释。

EncryptionProvider()

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

  • 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(); } 运行在我们称之为ThreadASP的内容中。
  • AzureEncryptionProvider()来电AzureEncryptionProvider()

然后事情就不同了。第一个令牌请求是多线程的:

  1. GetKeyAsync()返回Task
  2. 我们致电GetResult()阻止ThreadASP,直到GetKeyAsync()完成。
  3. GetKeyAsync()在另一个主题上调用GetKeyAsync()
  4. GetAccessToken()GetAccessToken()完成,释放ThreadASP。
  5. 我们的网页会返回给用户。好。
  6. GetAccessToken is running on its own thread.

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

    1. GetKeyAsync()在ThreadASP上调用GetKeyAsync()(不在单独的线程上。)
    2. GetAccessToken()会返回GetKeyAsync()
    3. 我们致电Task阻止ThreadASP,直到GetResult()完成。
    4. GetKeyAsync()必须等到ThreadASP空闲,ThreadASP必须等到GetAccessToken()完成,GetKeyAsync()必须等到GetKeyAsync()完成。哦,哦。
    5. 死锁。
    6. GetAccessToken is running on the same thread.

      为什么呢?谁知道?!?

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

      解决方案:完全异步

      为避免死锁,最佳做法是“一直使用异步”。当我们调用来自外部库的异步方法(例如Task)时尤其如此。重要的是不要强制该方法与Wait()ResultGetKeyAsync()同步。相反,请使用async and await,因为GetResult()会暂停该方法而不是阻止整个帖子。

      异步控制器操作

      await

      异步公共方法

      由于构造函数不能是异步的(因为异步方法必须返回public class HomeController : Controller { public async Task<ActionResult> Index() { var provider = new EncryptionProvider(); await provider.GetKeyBundle(); var x = provider.MyKeyBundle; return View(); } } ),我们可以将异步内容放入单独的公共方法中。

      Task

      神秘解决了。 :)这是帮助我理解的a final reference

      控制台应用

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

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

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

      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<string> 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 :(得分:-1)

我遇到了同样的挑战。我假设您还看到了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调用异步方法。