首页 文章

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

提问于
浏览
13

我按照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 回答

  • 23

    问题:死锁

    你的 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() .

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

    • GetKeyAsync() 返回Task .

    • 我们调用GetResult()阻止ThreadASP直到 GetKeyAsync() 完成 .

    • GetKeyAsync() 在另一个线程上调用 GetAccessToken() .

    • GetAccessToken()GetKeyAsync() 完成,释放ThreadASP .

    • 我们的网页返回给用户 . 好 .

    GetAccessToken is running on its own thread.

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

    • GetKeyAsync() 在ThreadASP上调用 GetAccessToken() (不在单独的线程上 . )

    • GetKeyAsync() 返回 Task .

    • 我们调用 GetResult() 阻止ThreadASP直到 GetKeyAsync() 完成 .

    • GetAccessToken() 必须等到ThreadASP空闲,ThreadASP必须等到 GetKeyAsync() 完成, GetKeyAsync() 必须等到 GetAccessToken() 完成 . 哦,哦 .

    • 死锁 .

    GetAccessToken is running on the same thread.

    为什么?谁知道?!?

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

    解决方案:一直向下异步

    为了避免死锁,这是一种最佳实践"to use async all the way down."当我们调用来自外部库的异步方法(例如 GetKeyAsync() )时尤其如此 . 重要的是不要通过与Wait()ResultGetResult() 同步强制该方法 . 相反,使用async and await因为 await 暂停方法而不是阻塞整个线程 .

    异步控制器操作

    public class HomeController : Controller
    {
        public async Task<ActionResult> 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<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;
        }
    }
    

    谜团已揭开 . :)这是a final reference,这有助于我的理解 .

    控制台应用程序

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

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

    在我的机器上,控制台应用程序运行了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();
                    }
                }
            }
        }
    }
    
  • -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调用异步方法 .

相关问题