C# · 12月 23, 2021

c# – Azure KeyVault Active Directory在异步调用时获取AcquisitionTokenAsync超时

我已经在我的ASP.Net MVC Web应用程序中安装了Azure Keyvault,它遵循Microsoft的 Hello Key Vault示例应用程序中的示例.

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 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来解决问题,但我不知道为什么.您还会注意到,我使用异步传递我的示例代码中的AuthenticationContext构造函数中的’null’而不是’TokenCache.DefaultShared’.这是强迫劫持立即而不是一个小时后过期.否则,您必须等待一个小时来重现行为.

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

解决方法 问题:僵局

您的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.
>我们的网页返回给用户.好.

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

> GetKeyAsync()在ThreadASP上调用GetAccessToken()(不在单独的线程上)
> GetKeyAsync()返回一个任务.
>我们调用GetResult()阻止ThreadASP直到GetKeyAsync()完成.
> GetAccessToken()必须等到ThreadASP是空闲的,ThreadASP必须等待GetKeyAsync()完成,GetKeyAsync()必须等到GetAccessToken()完成.呃哦
>死锁

为什么?谁知道?!?

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

解决方案:异步一路下来

为了避免僵局,最好的做法是“一直使用异步”.当我们调用异步方法(例如来自外部库的GetKeyAsync())时,这一点尤其如此.重要的是不要强制该方法与Wait(),Result或GetResult()同步.相反,使用async and 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 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 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(); } } } }}