Microsoftの Hello Key Vault サンプルアプリケーションの例に従って、ASP.Net MVCWebアプリケーションにAzureKeyvaultをセットアップしました。
Azure KeyVault(Active Directory)AuthenticationResultの有効期限はデフォルトで1時間です。したがって、1時間後、新しい認証トークンを取得する必要があります。 KeyVaultは、最初のAuthenticationResultトークンを取得してから最初の1時間は期待どおりに機能していますが、1時間の有効期限が切れると、新しいトークンを取得できません。
残念ながら、開発で1時間以上テストしたことがなかったため、これを実現するために本番環境で失敗しました。
とにかく、keyvaultコードの何が問題なのかを2日以上調べた後、すべての問題を修正するソリューションを思いつきました-非同期コードを削除します-しかし、それは非常にハックな感じがします。そもそもなぜうまくいかなかったのか知りたい。
私のコードは次のようになります:
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 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;
}
プログラムは、初めて実行したときに正常に動作します。そして1時間、AcquireTokenAsyncは同じ元の認証トークンを返します。これは素晴らしいことです。ただし、トークンの有効期限が切れると、AcquiteTokenAsyncは新しい有効期限の新しいトークンを取得する必要があります。そして、そうではありません-アプリケーションがハングするだけです。エラーは返されず、まったく何も返されませんでした。
したがって、AcquireTokenAsyncの代わりにAcquireTokenを呼び出すと問題は解決しますが、理由はわかりません。また、asyncを使用してサンプルコードのAuthenticationContextコンストラクターに「TokenCache.DefaultShared」ではなく「null」を渡していることにも気付くでしょう。これは、1時間後ではなく、すぐにトークを強制的に期限切れにするためです。それ以外の場合は、動作を再現するために1時間待つ必要があります。
これをまったく新しい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()
GetKeyAsync()
が完了するまでThreadASPをブロックすることを呼び出します。GetKeyAsync()
は別のスレッドでGetAccessToken()
を呼び出します。GetAccessToken()
およびGetKeyAsync()
が完了し、ThreadASPが解放されます。GetKeyAsync()
はThreadASPでGetAccessToken()
を呼び出します(別のスレッドではありません)。GetKeyAsync()
はTask
を返します。GetResult()
が完了するまで、GetKeyAsync()
ブロッキングThreadASPを呼び出します。GetAccessToken()
はThreadASPが解放されるまで待機する必要があり、ThreadASPはGetKeyAsync()
が完了するまで待機する必要があり、GetKeyAsync()
はGetAccessToken()
が完了するまで待機する必要があります。ええとああ。GetKeyAsync()
内には、アクセストークンキャッシュの状態に依存するフロー制御が必要です。フロー制御は、独自のスレッドでGetAccessToken()
を実行するかどうか、およびどの時点でTask
を返すかを決定します。
デッドロックを回避するには、「非同期を完全に使用する」ことをお勧めします。これは、外部ライブラリからGetKeyAsync()
などの非同期メソッドを呼び出す場合に特に当てはまります。 Wait()
、 Result
、またはGetResult()
と同期してメソッドを強制しないことが重要です。代わりに、 async
およびawait
を使用してください。これは、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 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;
}
}
_
謎が解けた。 :)ここに 最終参照 私の理解を助けました。
私の最初の答えはこのコンソールアプリでした。これは、最初のトラブルシューティング手順として機能しました。 問題は再現されませんでした。
コンソールアプリは5分ごとにループし、新しいアクセストークンを繰り返し要求します。各ループで、現在の時刻、有効期限、および取得したキーの名前を出力します。
私のマシンでは、コンソールアプリが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();
}
}
}
}
}
_