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

Aar*_*ron 15 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;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

该程序在我第一次运行时工作正常.并且一小时,AcquireTokenAsync返回相同的原始身份验证令牌,这很棒.但是一旦令牌到期,AcquiteTokenAsync应该获得一个新的令牌,其新的到期日期.它没有 - 应用程序只是挂起.没有错误返回,什么都没有.

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

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

Sha*_*tin 25

问题:死锁

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();
}
Run Code Online (Sandbox Code Playgroud)

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

  • AzureEncryptionProvider() 在我们称之为ThreadASP的内容中运行.
  • AzureEncryptionProvider()电话GetKeyAsync().

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

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

GetAccessToken正在自己的线程上运行.

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

  1. GetKeyAsync()调用GetAccessToken()ThreadASP(不在单独的线程上.)
  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<ActionResult> Index()
    {
        var provider = new EncryptionProvider();
        await provider.GetKeyBundle();
        var x = provider.MyKeyBundle;
        return View();
    }
}
Run Code Online (Sandbox Code Playgroud)

异步公共方法

由于构造函数不能是异步的(因为异步方法必须返回a 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<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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

控制台应用

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

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

在我的机器上,控制台应用程序运行了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<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();
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Aaron`GetAwaiter().GetResult()`不起作用,因为它阻塞线程,直到该线程上的任务*完成.这导致无法解决的情况. (2认同)