gpt4 book ai didi

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

转载 作者:可可西里 更新时间:2023-11-01 07:53:43 26 4
gpt4 key购买 nike

我按照 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 构造函数中,因此我将签名保留为异步,但删除了 wait 关键字。

其中包含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. 我们的网页返回给用户。很好。

GetAccessToken is running on its own thread.

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

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

GetAccessToken is running on the same thread.

为什么?谁知道?!?

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

解决方案:一直异步

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

异步 Controller 操作

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/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com