問題描述
我已按照 Microsoft
第二個令牌請求使用單線程:
GetKeyAsync()
在 ThreadASP 上調(diào)用GetAccessToken()
(不在單獨的線程上.)GetKeyAsync()
返回一個Task
.- 我們調(diào)用
GetResult()
阻塞 ThreadASP,直到GetKeyAsync()
完成. GetAccessToken()
必須等到 ThreadASP 空閑,ThreadASP 必須等到GetKeyAsync()
完成,GetKeyAsync()
必須等到GetAccessToken()
完成.哦哦.- 死鎖.
為什么?誰知道?!?
GetKeyAsync()
中必須有一些依賴于訪問令牌緩存狀態(tài)的流控制.流控制決定是否在自己的線程上運(yùn)行GetAccessToken()
,以及在什么時候返回Task
.
解決方案:一直異步
為避免死鎖,最佳做法是一直使用異步".當(dāng)我們調(diào)用來自外部庫的異步方法時尤其如此,例如 GetKeyAsync()
.重要的是不要強(qiáng)制方法與 Wait()
、結(jié)果
,或<代碼>GetResult().相反,請使用 async
和 await
因為 await
會暫停方法而不是阻塞整個線程.
異步控制器動作
公共類 HomeController : 控制器{公共異步任務(wù)<ActionResult>指數(shù)(){var provider = new EncryptionProvider();等待提供者.GetKeyBundle();var x = provider.MyKeyBundle;返回視圖();}}
異步公共方法
由于構(gòu)造函數(shù)不能是異步的(因為異步方法必須返回一個Task
),我們可以將異步的東西放到一個單獨的公共方法中.
公共類 EncryptionProvider{////認(rèn)證屬性省略公共 KeyBundle MyKeyBundle;公共加密提供者(){}公共異步任務(wù) GetKeyBundle(){var keyVaultClient = new KeyVaultClient(GetAccessToken);var keyBundleTask = 等待 keyVaultClient.GetKeyAsync(KeyVaultUrl, KeyVaultEncryptionKeyName);MyKeyBundle = keyBundleTask;}私有異步任務(wù)<字符串>獲取訪問令牌(字符串權(quán)限、字符串資源、字符串范圍){TokenCache.DefaultShared.Clear();//重現(xiàn)問題var authContext = new AuthenticationContext(authority, TokenCache.DefaultShared);var clientCredential = new ClientCredential(ClientIdWeb, ClientSecretWeb);var 結(jié)果 = 等待 authContext.AcquireTokenAsync(resource, clientCredential);var token = result.AccessToken;返回令牌;}}
謎團(tuán)解開了.:) 這是最終參考我的理解.
控制臺應(yīng)用
我最初的答案是這個控制臺應(yīng)用程序.它作為最初的故障排除步驟.它沒有重現(xiàn)問題.
控制臺應(yīng)用每五分鐘循環(huán)一次,反復(fù)請求新的訪問令牌.在每個循環(huán)中,它都會輸出當(dāng)前時間、到期時間和檢索到的密鑰的名稱.
在我的機(jī)器上,控制臺應(yīng)用程序運(yùn)行了 1.5 小時,并在原始過期后成功檢索到密鑰.
使用系統(tǒng);使用 System.Collections.Generic;使用 System.Threading.Tasks;使用 Microsoft.Azure.KeyVault;使用 Microsoft.IdentityModel.Clients.ActiveDirectory;命名空間 ConsoleApp{課堂節(jié)目{私有靜態(tài)異步任務(wù) RunSample(){var keyVaultClient = new KeyVaultClient(GetAccessToken);//創(chuàng)建一個密鑰 :)var keyCreate = 等待 keyVaultClient.CreateKeyAsync(保險庫:_keyVaultUrl,密鑰名稱:_keyVaultEncryptionKeyName,密鑰類型:_keyType,關(guān)鍵屬性:新的關(guān)鍵屬性(){啟用 = 真,過期 = UnixEpoch.FromUnixTime(int.MaxValue),NotBefore = UnixEpoch.FromUnixTime(0),},技術(shù)標(biāo)簽: 新字典<字符串、字符串>{{ "目的", "StackOverflow 演示" }});Console.WriteLine(string.Format(已創(chuàng)建 {0}",keyCreate.KeyIdentifier.Name));//取回密鑰var keyRetrieve = 等待 keyVaultClient.GetKeyAsync(_keyVaultUrl,_keyVaultEncryptionKeyName);Console.WriteLine(string.Format("檢索到 {0}",keyRetrieve.KeyIdentifier.Name));}私有靜態(tài)異步任務(wù)<字符串>獲取訪問令牌(字符串權(quán)限、字符串資源、字符串范圍){var clientCredential = 新的 ClientCredential(_keyVaultAuthClientId,_keyVaultAuthClientSecret);var context = new AuthenticationContext(權(quán)威,TokenCache.DefaultShared);var result = await context.AcquireTokenAsync(resource, clientCredential);_expiresOn = 結(jié)果.ExpiresOn.DateTime;Console.WriteLine(DateTime.UtcNow.ToShortTimeString());Console.WriteLine(_expiresOn.ToShortTimeString());返回結(jié)果.AccessToken;}私有靜態(tài)日期時間_expiresOn;私有靜態(tài)字符串_keyVaultAuthClientId = "xxxxx-xxx-xxxxx-xxx-xxxxx",_keyVaultAuthClientSecret = "xxxxx-xxx-xxxxx-xxx-xxxxx",_keyVaultEncryptionKeyName = "MYENCRYPTIONKEY",_keyVaultUrl = "https://xxxxx.vault.azure.net/",_keyType = "RSA";靜態(tài)無效主要(字符串 [] 參數(shù)){var keepGoing = true;同時(繼續(xù)){RunSample().GetAwaiter().GetResult();//休眠五分鐘System.Threading.Thread.Sleep(new TimeSpan(0, 5, 0));如果(日期時間.UtcNow > _expiresOn){Console.WriteLine("---過期---");Console.ReadLine();}}}}}
I have setup Azure Keyvault on my ASP.Net MVC web application by following the example in Microsoft's Hello Key Vault sample application.
Azure KeyVault (Active Directory) AuthenticationResult by default has a one hour expiry. So after one hour, you must get a new authentication token. KeyVault is working as expected for the first hour after getting my first AuthenticationResult token, but after the 1 hour expiry, it fails to get a new token.
Unfortunately it took a failure on my production environment for me to realize this, as I never tested past one hour in development.
Anyways, after over two days of trying to figure out what was wrong with my keyvault code, I came up with a solution that fixes all of my problems - remove the asynchronous code - but it feels very hacky. I want to find out why it was not working in the first place.
My code looks like this:
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;
}
The GetAccessToken method signature has to be asynchronous to pass into the new KeyVaultClient constructor, so I left the signature as async, but I removed the await keyword.
With the await keyword in there (the way it should be, and is in the sample):
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;
}
The program works fine the first time I run it. And for an hour, AcquireTokenAsync returns the same original authentication token which is great. But once the token expires, AcquiteTokenAsync should get a new token with a new expiry date. And it doesn't - the application just hangs. No error returned, nothing at all.
So calling AcquireToken instead of AcquireTokenAsync solves the problem, but I have no idea why. You'll also notice that I'm passing 'null' instead of 'TokenCache.DefaultShared' into the AuthenticationContext constructor in my sample code with async. This is to force the toke to expire immediately instead of after one hour. Otherwise, you have to wait an hour to reproduce the behavior.
I was able to reproduce this again in a brand new MVC project, so I don't think it has anything to do with my specific project. Any insight would be appreciated. But for now, I'm just not using async.
Problem: deadlock
Your EncryptionProvider()
is calling GetAwaiter().GetResult()
. This blocks the thread, and on subsequent token requests, causes a deadlock. The following code is the same as yours is but separates things to facilitate explanation.
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();
}
In both token requests, the execution starts in the same way:
AzureEncryptionProvider()
runs in what we'll call ThreadASP.AzureEncryptionProvider()
callsGetKeyAsync()
.
Then things differ. The first token request is multi-threaded:
GetKeyAsync()
returns aTask
.- We call
GetResult()
blocking ThreadASP untilGetKeyAsync()
completes. GetKeyAsync()
callsGetAccessToken()
on another thread.GetAccessToken()
andGetKeyAsync()
complete, freeing ThreadASP.- Our web page returns to the user. Good.
The second token request uses a single thread:
GetKeyAsync()
callsGetAccessToken()
on ThreadASP (not on a separate thread.)GetKeyAsync()
returns aTask
.- We call
GetResult()
blocking ThreadASP untilGetKeyAsync()
completes. GetAccessToken()
must wait until ThreadASP is free, ThreadASP must wait untilGetKeyAsync()
completes,GetKeyAsync()
must wait untilGetAccessToken()
completes. Uh oh.- Deadlock.
Why? Who knows?!?
There must be some flow control within GetKeyAsync()
that relies on the state of our access token cache. The flow control decides whether to run GetAccessToken()
on its own thread and at what point to return the Task
.
Solution: async all the way down
To avoid a deadlock, it is a best practice "to use async all the way down." This is especially true when we are calling an async method, such as GetKeyAsync()
, that is from an external library. It is important not force the method to by synchronous with Wait()
, Result
, or GetResult()
. Instead, use async
and await
because await
pauses the method instead of blocking the whole thread.
Async controller action
public class HomeController : Controller
{
public async Task<ActionResult> Index()
{
var provider = new EncryptionProvider();
await provider.GetKeyBundle();
var x = provider.MyKeyBundle;
return View();
}
}
Async public method
Since a constructor cannot be async (because async methods must return a Task
), we can put the async stuff into a separate public method.
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;
}
}
Mystery solved. :) Here is a final reference that helped my understanding.
Console App
My original answer had this console app. It worked as an initial troubleshooting step. It did not reproduce the problem.
The console app loops every five minutes, repeatedly asking for a new access token. At each loop, it outputs the current time, the expiry time, and the name of the retrieved key.
On my machine, the console app ran for 1.5 hours and successfully retrieved a key after expiration of the original.
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();
}
}
}
}
}
這篇關(guān)于異步調(diào)用時 Azure KeyVault Active Directory AcquireTokenAsync 超時的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網(wǎng)!