异步调用时,Azure KeyVault Active Directory AcquireTokenAsync超时
我按照Microsoft的Hello Key Vault示例应用程序中的示例在我的ASP.Net MVC Web应用程序上设置了Azure Keyvault。
Azure KeyVault(Active Directory)AuthenticationResult默认情况下有一个小时到期。 因此,一小时后,您必须获得一个新的身份validation令牌。 在获得我的第一个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 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 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返回相同的原始身份validation令牌,这很棒。 但是一旦令牌到期,AcquiteTokenAsync应该获得一个新的令牌,其新的到期日期。 它没有 – 应用程序只是挂起。 没有错误返回,什么都没有。
所以调用AcquireToken而不是AcquireTokenAsync解决了这个问题,但我不明白为什么。 你还会注意到我在我的示例代码中使用async将’null’而不是’TokenCache.DefaultShared’传递给AuthenticationContext构造函数。 这是为了迫使toke立即过期而不是一小时后。 否则,您必须等待一个小时才能重现该行为。
我能够在一个全新的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()
。
事情就不一样了。 第一个令牌请求是multithreading的:
-
GetKeyAsync()
返回一个Task
。 - 我们调用
GetResult()
阻止ThreadASP,直到GetKeyAsync()
完成。 -
GetKeyAsync()
在另一个线程上调用GetAccessToken()
。 -
GetAccessToken()
和GetKeyAsync()
完成,释放ThreadASP。 - 我们的网页返回给用户。 好。
第二个令牌请求使用单个线程:
-
GetKeyAsync()
在ThreadASP上调用GetAccessToken()
(不在单独的线程上)。 -
GetKeyAsync()
返回一个Task
。 - 我们调用
GetResult()
阻止ThreadASP,直到GetKeyAsync()
完成。 -
GetAccessToken()
必须等到ThreadASP空闲,ThreadASP必须等到GetKeyAsync()
完成,GetKeyAsync()
必须等到GetAccessToken()
完成。 哦,哦。 - 僵局。
为什么? 谁知道?!?
GetKeyAsync()
中必须有一些流控制依赖于我们的访问令牌缓存的状态。 流控制决定是在其自己的线程上运行GetAccessToken()
以及在何时返回Task
。
解决方案:异步完全向下
为避免死锁,最佳做法是“一直使用异步”。 当我们调用来自外部库的异步方法(例如GetKeyAsync()
时尤其如此。 重要的是不要通过与Wait()
, Result
或GetResult()
同步来强制该方法。 相反,使用async
和await
因为await
暂停方法而不是阻塞整个线程。
异步控制器操作
public class HomeController : Controller { public async Task 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 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; } }
谜团已揭开。 :)这是一个有助于我理解的最终参考 。
控制台应用
我的原始答案有这个控制台应用程序 它作为初始故障排除步骤。 它没有重现这个问题。
控制台应用程序每五分钟循环一次,反复询问新的访问令牌。 在每个循环中,它输出当前时间,到期时间和检索到的密钥的名称。
在我的机器上,控制台应用程序运行了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 { { "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 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(); } } } } }
我有同样的挑战。 我假设您还看到了样本发布在https://azure.microsoft.com/en-us/documentation/articles/key-vault-use-from-web-application/
该示例与我的代码所做的事情之间存在很大差异(我认为代码的意图是)。 在示例中,它们检索secrete并将其作为Utils类的静态成员存储在Web应用程序中。 因此,该示例在应用程序的整个运行时间内检索一次秘密。
就我而言,我在应用程序运行时的不同时间为不同目的检索不同的密钥。
此外,您链接到的示例下载使用X.509证书来validationWeb应用程序到KeyVault,而不是客户端密钥。 这也有可能存在问题。
我看到与@ shaun-luttin的聊天结论你导致了僵局,但这不是我想的全部故事。 我不使用.GetAwaiter()。GetResult()或从ctor调用异步方法。
- 如何在ASP.NET中创建自动完成TextBox?
- HttpContext.Current.Request.Form复选框
- 移动请求检测和重定向到Asp中的移动页面(Active Server Pages)
- 部分视图中的ASP.NET MVCvalidation并返回到父视图
- asp.net身份userName是唯一的吗?
- 如何使用ASP.NET检测页面关闭事件
- 在没有第三方框架的情况下从HttpWebResponse反序列化JSON的方法
- 如何返回false,如何创建将重定向到Login的自定义属性,类似于Authorize属性 – ASP.NET MVC
- 使用ASP.Net MVC RouteConfig的AngularJS Ui-Router。 它是如何工作的?