草庐IT

c# - 异步调用时 Azure KeyVault Active Directory AcquireTokenAsync 超时

coder 2024-05-22 原文

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

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

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

无论如何,在尝试弄清楚我的 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 构造函数中,因此我将签名保留为异步,但删除了 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 返回相同的原始身份验证 token ,这很棒。但是一旦 token 过期,AcquiteTokenAsync 应该获得一个具有新到期日期的新 token 。它没有 - 应用程序只是挂起。没有错误返回,什么都没有。

因此调用 AcquireToken 而不是 AcquireTokenAsync 解决了问题,但我不知道为什么。您还会注意到,在我的异步示例代码中,我将“null”而不是“TokenCache.DefaultShared”传递给 AuthenticationContext 构造函数。这是为了强制 token 立即过期,而不是在一小时后过期。否则,您必须等待一个小时才能重现该行为。

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

最佳答案

问题:死锁

您的 EncryptionProvider() 正在调用 GetAwaiter().GetResult() .这会阻塞线程,并在随后的 token 请求中导致死锁。以下代码与您的代码相同,但将内容分开以便于解释。

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

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

  • AzureEncryptionProvider() 在我们称为 ThreadASP 的环境中运行。
  • AzureEncryptionProvider() 调用 GetKeyAsync()

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

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

第二个 token 请求使用单线程:

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

为什么?谁知道?!?

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

解决方案:一直异步

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

异步 Controller Action

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

异步公共(public)方法

由于构造函数不能是异步的(因为异步方法必须返回一个Task),我们可以将异步内容放入一个单独的公共(public)方法中。

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这有助于我的理解。

控制台应用

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

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

在我的机器上,控制台应用程序运行了 1.5 小时,并在原始 key 过期后成功检索到 key 。

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

关于c# - 异步调用时 Azure KeyVault Active Directory AcquireTokenAsync 超时,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32594642/

有关c# - 异步调用时 Azure KeyVault Active Directory AcquireTokenAsync 超时的更多相关文章

  1. ruby - 简单获取法拉第超时 - 2

    有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url

  2. c# - 如何在 ruby​​ 中调用 C# dll? - 2

    如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

  3. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  4. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  5. ruby-on-rails - Rails 优雅地处理超时 session ? - 2

    使用rails4,ruby2。我在rails配置中为我的cookiesession设置了30分钟的超时时间。问题是,如果我转到表单,让session超时,然后提交表单,我会收到此ActionController::InvalidAuthenticityToken错误。如何在Rails中优雅地处理这个错误?比如说,重定向到登录屏幕? 最佳答案 在您的ApplicationController:rescue_fromActionController::InvalidAuthenticityTokendoredirect_tosome_p

  6. Ruby 在 n *milli* 秒后超时一段代码 - 2

    在Ruby中,我需要在n毫秒秒后暂停一段代码的执行。我知道RubyTimeout库支持秒的超时:http://ruby-doc.org/stdlib/libdoc/timeout/rdoc/index.html这可能吗? 最佳答案 只需为超时使用十进制值。n毫秒的示例:Timeout::timeout(n/1000.0){sleep(100)} 关于Ruby在n*milli*秒后超时一段代码,我们在StackOverflow上找到一个类似的问题: https:

  7. c# - C# 中的 Flatten Ruby 方法 - 2

    我如何做Ruby方法"Flatten"RubyMethod在C#中。此方法将锯齿状数组展平为一维数组。例如:s=[1,2,3]#=>[1,2,3]t=[4,5,6,[7,8]]#=>[4,5,6,[7,8]]a=[s,t,9,10]#=>[[1,2,3],[4,5,6,[7,8]],9,10]a.flatten#=>[1,2,3,4,5,6,7,8,9,10 最佳答案 递归解决方案:IEnumerableFlatten(IEnumerablearray){foreach(variteminarray){if(itemisIEnume

  8. ruby - 从 ruby​​ 调用时返回 shell 脚本的状态值? - 2

    我希望这些值匹配。当shell脚本由于某些错误条件而退出时(因此返回非零值),它们不匹配。壳$?返回1,ruby$?返回256。>>%x[lskkr]ls:kkr:Nosuchfileordirectory=>"">>puts$?256=>nil>>exitHadoop:~Madcap$lskkrls:kkr:NosuchfileordirectoryHadoop:~Madcap$echo$?1 最佳答案 在Ruby中$?是一个Process::Status实例。打印$?等同于调用$?.to_s,这等同于$?.to_i.to_s(来

  9. ruby - 可以像在 C# 中使用#region 一样在 Ruby 中使用 begin/end 吗? - 2

    我最近从C#转向了Ruby,我发现自己无法制作可折叠的标记代码区域。我只是想到做这种事情应该没问题:classExamplebegin#agroupofmethodsdefmethod1..enddefmethod2..endenddefmethod3..endend...但是这样做真的可以吗?method1和method2最终与method3是同一种东西吗?还是有一些我还没有见过的用于执行此操作的Ruby惯用语? 最佳答案 正如其他人所说,这不会改变方法定义。但是,如果要标记方法组,为什么不使用Ruby语义来标记它们呢?您可以使用

  10. c# - Ruby 等效于 C# Linq 聚合方法 - 2

    什么是Linq聚合方法的ruby​​等价物。它的工作原理是这样的varfactorial=new[]{1,2,3,4,5}.Aggregate((acc,i)=>acc*i);每次将数组序列中的值传递给lambda时,变量acc都会累积。 最佳答案 这在数学以及几乎所有编程语言中通常称为折叠。它是更普遍的变形概念的一个实例。Ruby从Smalltalk中继承了这个特性的名称,它被称为inject:into:(像aCollectioninject:aStartValueinto:aBlock一样使用。)所以,在Ruby中,它称为inj

随机推荐