当异步调用Azure的KeyVault的Active Directory AcquireTokenAsync超时 [英] Azure KeyVault Active Directory AcquireTokenAsync timeout when called asynchronously

查看:821
本文介绍了当异步调用Azure的KeyVault的Active Directory AcquireTokenAsync超时的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经安装Azure的Keyvault我的ASP.Net MVC Web应用程序遵循微软的你好重点库的示例应用程序。

I have setup Azure Keyvault on my ASP.Net MVC web application by following the example in Microsoft's Hello Key Vault sample application.

天青KeyVault(Active Directory)中默认AuthenticationResult有一个小时的有效期。因此,一小时后,你必须得到一个新的身份验证令牌。 KeyVault正按预期获取我的第一个AuthenticationResult标记后的第一个小时,但1小时期满后,它不能获得新令牌。

Azure KeyVault (Active Directory) AuthenticationResult by default has a one hour expiry. So after one hour, you must get a new authentication token. KeyVault is working as expected for the first hour after getting my first AuthenticationResult token, but after the 1 hour expiry, it fails to get a new token.

不幸的是,发生在我的生产环境中的失败让我意识到这一点,因为我从来没有在发展测试过去一个小时。

Unfortunately it took a failure on my production environment for me to realize this, as I never tested past one hour in development.

不管怎么说,过两人后的天试图找出什么是错的我keyvault code,我想出了解决我的所有问题的解决方案 - 去除异步code - 但感觉很哈克。我想找出为什么没有摆在首位的工作。

Anyways, after over two days of trying to figure out what was wrong with my keyvault code, I came up with a solution that fixes all of my problems - remove the asynchronous code - but it feels very hacky. I want to find out why it was not working in the first place.

我的code是这样的:

My code looks like this:

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构造函数,所以我离开了签名异步的,但我删除了的await关键字。

The GetAccessToken method signature has to be asynchronous to pass into the new KeyVaultClient constructor, so I left the signature as async, but I removed the await keyword.

通过在那里等待关键字(的事情应该是这样,而且在样品中):

With the await keyword in there (the way it should be, and is in the sample):

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应该得到一个新的到期日一个新的令牌。而且它不会 - 应用程序只是挂起。没有错误返回,什么都没有。

The program works fine the first time I run it. And for an hour, AcquireTokenAsync returns the same original authentication token which is great. But once the token expires, AcquiteTokenAsync should get a new token with a new expiry date. And it doesn't - the application just hangs. No error returned, nothing at all.

于是打电话AcquireToken而不是AcquireTokenAsync解决了这个问题,但我不知道为什么。您还会注意到,我通过'零'而不是'TokenCache.DefaultShared入AuthenticationContext构造在我的示例code。与异步。这是迫使托克一小时后,而不是立即的失效。否则,您必须等待一个小时重现行为。

So calling AcquireToken instead of AcquireTokenAsync solves the problem, but I have no idea why. You'll also notice that I'm passing 'null' instead of 'TokenCache.DefaultShared' into the AuthenticationContext constructor in my sample code with async. This is to force the toke to expire immediately instead of after one hour. Otherwise, you have to wait an hour to reproduce the behavior.

我能够在一个全新的MVC项目再次重现这一点,所以我不认为这有什么与我的具体项目。任何有识之士将AP preciated。但现在,我只是没有使用异步。

I was able to reproduce this again in a brand new MVC project, so I don't think it has anything to do with my specific project. Any insight would be appreciated. But for now, I'm just not using async.

推荐答案

EncryptionProvider()正在呼叫<一个href=\"http://stackoverflow.com/questions/17284517/is-task-result-the-same-as-getawaiter-getresult\"><$c$c>GetAwaiter().GetResult().这将阻止线程,并在后续的令牌请求,导致死锁。下面code是一样的你不过是分开的东西为了说明的方便。

Problem: deadlock

Your EncryptionProvider() is calling GetAwaiter().GetResult(). This blocks the thread, and on subsequent token requests, causes a deadlock. The following code is the same as yours is but separates things to facilitate explanation.

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()

  • In both token requests, the execution starts in the same way:

    • AzureEncryptionProvider() runs in what we'll call ThreadASP.
    • AzureEncryptionProvider() calls GetKeyAsync().

      1. GetKeyAsync()返回<一个href=\"https://msdn.microsoft.com/en-us/library/system.threading.tasks.task%28v=vs.110%29.aspx\"><$c$c>Task.

      2. 我们称之为<一个href=\"https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult%28v=vs.110%29.aspx\"><$c$c>GetResult()阻塞ThreadASP直到 GetKeyAsync()完成。

      3. GetKeyAsync()通话 GetAccessToken()另一个线程。

      4. GetAccessToken() GetKeyAsync()完整,释放ThreadASP。

      5. 我们的网页返回给用户。不错。

      1. GetKeyAsync() returns a Task.
      2. We call GetResult() blocking ThreadASP until GetKeyAsync() completes.
      3. GetKeyAsync() calls GetAccessToken() on another thread.
      4. GetAccessToken() and GetKeyAsync() complete, freeing ThreadASP.
      5. Our web page returns to the user. Good.


      1. GetKeyAsync()通话 GetAccessToken()上ThreadASP(而不是在一个单独的线程。)

      2. GetKeyAsync()返回工作

      3. 我们称之为调用getResult()阻塞ThreadASP直到 GetKeyAsync()完成。

      4. GetAccessToken()必须等到ThreadASP是免费的,ThreadASP必须等到 GetKeyAsync()完成后, GetKeyAsync()必须等到 GetAccessToken()完成。呃哦。

      5. 僵局。

      1. GetKeyAsync() calls GetAccessToken() on ThreadASP (not on a separate thread.)
      2. GetKeyAsync() returns a Task.
      3. We call GetResult() blocking ThreadASP until GetKeyAsync() completes.
      4. GetAccessToken() must wait until ThreadASP is free, ThreadASP must wait until GetKeyAsync() completes, GetKeyAsync() must wait until GetAccessToken() completes. Uh oh.
      5. Deadlock.

      有必须在一些流量控制GetKeyAsync(),依赖于我们的访问令牌缓存的状态。流量控制决定是否运行 GetAccessToken()在自己的线程,并在什么时候返回工作

      There must be some flow control within GetKeyAsync() that relies on the state of our access token cache. The flow control decides whether to run GetAccessToken() on its own thread and at what point to return the Task.

      要避免死锁,这是一个最佳实践使用异步一路下跌。当我们呼吁异步方法,如这是特别真实GetKeyAsync(),也就是从外部库。通过同步的 不强制方法来等待()是非常重要的 结果 ,或调用getResult()。相反,使用 异步等待 因为等待暂停方法,而不是阻止整个线程。

      To avoid a deadlock, it is a best practice "to use async all the way down." This is especially true when we are calling an async method, such as GetKeyAsync(), that is from an external library. It is important not force the method to by synchronous with Wait(), Result, or GetResult(). Instead, use async and await because await pauses the method instead of blocking the whole thread.

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

      异步公共方法

      由于构造函数不能是异步的(因为异步方法必须返回一个工作),我们可以把异步的东西到一个单独的公共方法。

      Async public method

      Since a constructor cannot be async (because async methods must return a Task), we can put the async stuff into a separate public method.

      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;
          }
      }
      

      解开了谜底。 :)下面是最终参考,帮助我的理解

      我原来的答复是这样的控制台应用程序。它的工作作为初始故障诊断步骤。的它没有重现该问题。

      My original answer had this console app. It worked as an initial troubleshooting step. It did not reproduce the problem.

      控制台应用程序循环,每隔五分钟,反复询问新的访问令牌。在每个循环中,它输出当前时间,期满时间,并且所检索的项的名称。

      The console app loops every five minutes, repeatedly asking for a new access token. At each loop, it outputs the current time, the expiry time, and the name of the retrieved key.

      在我的机器上,控制台应用程序运行1.5小时,原到期后取回成功的关键。

      On my machine, the console app ran for 1.5 hours and successfully retrieved a key after expiration of the original.

      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<string, string> {
                          { "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<string> 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();
                      }
                  }
              }
          }
      }
      

      这篇关于当异步调用Azure的KeyVault的Active Directory AcquireTokenAsync超时的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆