Ответ 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()
.
Тогда все по-другому. Первый запрос на токен многопоточен:
-
GetKeyAsync()
возвращаетTask
. - Мы вызываем
GetResult()
, блокируя ThreadASP до тех пор, покаGetKeyAsync()
не завершится. -
GetKeyAsync()
вызываетGetAccessToken()
в другом потоке. -
GetAccessToken()
иGetKeyAsync()
завершение, освобождение ThreadASP. - Наша веб-страница возвращается пользователю. Хорошо.
Второй запрос на токен использует один поток:
-
GetKeyAsync()
вызываетGetAccessToken()
в ThreadASP (не в отдельном потоке.) -
GetKeyAsync()
возвращает aTask
. - Мы вызываем
GetResult()
, блокируя ThreadASP до тех пор, покаGetKeyAsync()
не завершится. -
GetAccessToken()
должен ждать, пока ThreadASP не будет освобожден, ThreadASP должен дождаться завершенияGetKeyAsync()
,GetKeyAsync()
должен дождаться завершенияGetAccessToken()
. Ох. - Тупик.
Почему? Кто знает?!?
В GetKeyAsync()
должно быть некоторое управление потоком, которое зависит от состояния кеша токена доступа. Управление потоком решает, следует ли запускать GetAccessToken()
в своем потоке и в какой точке вернуть Task
.
Решение: асинхронно до конца
Чтобы избежать тупика, лучше всего использовать async до конца. Это особенно актуально, когда мы вызываем метод async, например GetKeyAsync()
, который находится из внешней библиотеки. Важно не форсировать метод синхронно с Wait()
, Result
, или GetResult()
. Вместо этого используйте async
и await
, потому что await
приостанавливает этот метод вместо блокировки всего потока.
Действие контроллера Async
public class HomeController : Controller
{
public async Task<ActionResult> Index()
{
var provider = new EncryptionProvider();
await provider.GetKeyBundle();
var x = provider.MyKeyBundle;
return View();
}
}
Открытый метод Async
Поскольку конструктор не может быть асинхронным (поскольку методы async должны возвращать a Task
), мы можем поместить материал async в отдельный публичный метод.
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;
}
}
Тайна решена.:) Вот окончательная ссылка, которая помогла мне понять.
Консольное приложение
В моем первоначальном ответе было это консольное приложение. Он работал как первый шаг по устранению неполадок. Он не воспроизводил проблему.
Консольное приложение петли каждые пять минут, многократно запрашивая новый токен доступа. В каждом цикле он выводит текущее время, время истечения срока действия и имя извлеченного ключа.
На моей машине консольное приложение выполнялось в течение 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();
}
}
}
}
}