【问题标题】:Azure KeyVault Active Directory AcquireTokenAsync timeout when called asynchronously异步调用时 Azure KeyVault Active Directory AcquireTokenAsync 超时
【发布时间】:2015-12-12 05:11:22
【问题描述】:

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

默认情况下,Azure KeyVault (Active Directory) AuthenticationResult 的有效期为一小时。因此,一小时后,您必须获得一个新的身份验证令牌。 KeyVault 在获得我的第一个 AuthenticationResult 令牌后的第一个小时内按预期工作,但在 1 小时到期后,它无法获得新令牌。

不幸的是,我在生产环境中遇到了故障才意识到这一点,因为我从未在开发中测试超过一小时。

无论如何,经过两天多的尝试找出我的 keyvault 代码出了什么问题,我想出了一个解决方案来解决我的所有问题 - 删除异步代码 - 但它感觉非常糟糕。我想知道为什么它一开始就不起作用。

我的代码如下所示:

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 关键字。

使用 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 可以解决问题,但我不知道为什么。您还会注意到,我在示例代码中使用异步将“null”而不是“TokenCache.DefaultShared”传递到 AuthenticationContext 构造函数中。这是为了强制令牌立即过期,而不是在一小时后过期。否则,您必须等待一个小时才能重现该行为。

我能够在一个全新的 MVC 项目中再次重现这一点,所以我认为这与我的特定项目无关。任何见解将不胜感激。但现在,我只是不使用异步。

【问题讨论】:

  • 要重现该行为,您还可以调用 context.TokenCache.Clear() 而不是将 null 传递给构造函数。
  • 嗨,肖恩,我正在使用 Azure SKD 2.7。而且我可以使用 context.TokenCache.Clear() 成功重现我的问题,而不是将 null 传递给构造函数。您也是正确的,问题是 AcquireTokenAsync() 一旦原始令牌过期就会挂起,解决方法是使用非异步方法。我将在下面尝试您的解决方案,看看它是否能解决我的问题。
  • 这里要注意的另一件事是,在 Azure 网站上,这仅在 Always On 为真时发生 - 这意味着如果允许 IIS 关闭该网站,则该网站将重置并在该网站之后再次工作旋转回来。然后它会在运行一个小时后再次停止工作。
  • 是的,我不在乎令牌是否实际过期。我只想每次都强制获得一个新的,因为在请求第二个令牌期间发生故障。
  • 在我的生产环境中,我使用依赖注入,但是我能够在没有 DI 的全新 MVC 项目中重现这一点。我通过简单地创建类的实例并调用加密方法在控制器 ActionResult 方法中使用它。感谢您在这方面的帮助 - 我真的很想知道为什么这不起作用。我不介意使用非异步方法,我只是不明白为什么异步不起作用。

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


【解决方案1】:

问题:死锁

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

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

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

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

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

为什么?谁知道?!?

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

解决方案:一直异步

为避免死锁,最佳做法是“一直使用异步”。当我们调用来自外部库的异步方法(例如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();
                }
            }
        }
    }
}

【讨论】:

  • 我可以将它复制到一个新的控制台项目中并运行它,它运行得非常好。但是,从 MVC 应用程序中运行相同的代码不会。所以我相信这个问题与它在 MVC 应用程序中的事实有关。需要注意的另一件有趣的事情是,在 MVC 应用程序中,我能够在单个请求中一遍又一遍地循环 AcquireTokenAsync 行(使用 for 循环测试)并且它工作正常。但是一旦第二个 Web 请求命中,获取令牌就会失败。
  • 对不起 - 它的失败方式与我最初的问题失败的方式相同。它在第二次尝试异步获取新的访问令牌时挂起。从字面上看,什么都没有发生,程序只是坐在那里等待和等待。什么都没有返回,没有任何崩溃。它似乎也与 Web 请求有关,因为我可以从单个 Web 请求中生成许多新令牌。但是对服务器的第二个请求会导致它挂起。
  • 但我在这里看到 [msdn.microsoft.com/en-us/magazine/jj991977.aspx] 混合阻塞和异步代码是一个很大的问题 - 所以我不会这样做
  • @Aaron GetAwaiter().GetResult() 不起作用,因为它会阻塞线程,直到 该线程上的任务 完成。这会导致无法解决的情况。
  • @ShaunLuttin 我现在遇到了完全相同的问题,在寻找解决方案时,我偶然发现了您的有用答案。就我而言,我通过调用'Task.Run(() => keyVaultClient.GetKeyAsync(_keyVaultUrl, _keyVaultEncryptionKeyName)).GetAwaiter().GetResult()'解决了这个问题。但是我现在想知道,为什么在这一行中添加 '.ConfigureAwait(false)' 是不够的:'var result = await context.AcquireTokenAsync(resource, clientCredential)' 也许你可以给我解释一下原因?跨度>
【解决方案2】:

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

的示例

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

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

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

我看到与@shaun-luttin 的聊天得出结论是你造成了僵局,但我认为这不是全部。我不使用 .GetAwaiter().GetResult() 或从 ctor 调用异步方法。

【讨论】:

  • 我已将控制台应用程序重新放入答案中。如果这对你有帮助的话,现在让我来。
  • 恐怕不是...我也在使用MVC。 2 小时后,应用停止对 KV 的身份验证。事实上,我确实做了一些改变,并“一直异步”,但没有产生任何影响。我也会在可能的时候发布代码,但这就像 OP 一样。
  • 让控制台应用程序连续运行两三个小时是否可以工作?
  • 我刚刚在我的机器上再次运行它,它仍然工作了 2 个小时。
  • 快速更新:还没来得及尝试,但我会的
猜你喜欢
  • 1970-01-01
  • 2022-09-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-13
相关资源
最近更新 更多