在 Web 应用程序中使用 KeyVault 的示例代码包含以下代码:
public static async Task<string> GetSecret(string secretId)
{
var secret = await keyVaultClient.GetSecretAsync(secretId);
return secret.Value;
}
我已将示例中包含的
KeyVaultAccessor
对象合并到我的应用程序中以对其进行测试。该调用作为对我的 Web API 控制器方法之一的查询的一部分来执行:
var secret = KeyVaultAccessor.GetSecret("https://superSecretUri").Result;
不幸的是,调用永远不会返回并且查询无限期挂起......
可能是什么原因,因为坦白说我不知道从哪里开始......?
这是我在我的博客上完整描述的常见死锁问题。简而言之,async
方法在
await
完成后尝试返回 ASP.NET 请求上下文,但该请求一次只允许一个线程,并且该上下文中已经存在一个线程(被阻止的线程)致电
Result
)。因此,任务正在等待上下文空闲,而线程正在阻塞上下文,直到任务完成:死锁。正确的解决方案是使用
await
而不是
Result
:
var secret = await KeyVaultAccessor.GetSecret("https://superSecretUri");
var secret = Task.Run(async () => await KeyVaultAccessor.GetSecretAsync("https://superSecretUri")).Result;
如果您使用非异步方法,这仍然可以让您使用
.Result
不幸的是,调用永远不会返回并且查询无限期挂起......你遇到了典型的僵局。这就是为什么
你不应该阻塞异步代码。在幕后,编译器生成一个状态机并捕获称为 SynchronizationContext
的东西。当您同步阻止调用线程时,尝试将延续发布回同一上下文会导致死锁。不要使用
.Result
同步阻塞,而是让控制器异步并等待从
Task
返回的
GetSecret
:
public async IHttpActionResult FooAsync()
{
var secret = await KeyVaultAccessor.GetSecretAsync("https://superSecretUri");
return Ok();
}
旁注 - 异步方法应遵循命名约定并以 Async
为后缀。
Response<KeyVaultSecret> Response = await SecretClient.GetSecretAsync("SecretName");
调用Response.Value,不返回秘密值;相反,它返回一个 KeyVaultSecret 对象。
KeyVaultSecret Secret = Response.Value;
检索秘密值:
string SecretValue = Secret.Value;
我写这个答案是因为我一直将 Response.Value 传递给 API,认为我正在传递秘密值。难怪我收到了 403 Forbidden 错误。
public class AzureKeyVaultClient
{
public string GetSecret(string name, string vault)
{
var client = new RestClient($"https://{vault}.vault.azure.net/");
client.Authenticator = new AzureAuthenticator($"https://vault.azure.net");
var request = new RestRequest($"secrets/{name}?api-version=2016-10-01");
request.Method = Method.GET;
var result = client.Execute(request);
if (result.StatusCode != HttpStatusCode.OK)
{
Trace.TraceInformation($"Unable to retrieve {name} from {vault} with response {result.Content}");
var exception = GetKeyVaultErrorFromResponse(result.Content);
throw exception;
}
else
{
return GetValueFromResponse(result.Content);
}
}
public string GetValueFromResponse(string content)
{
var result = content.FromJson<keyvaultresponse>();
return result.value;
}
public Exception GetKeyVaultErrorFromResponse(string content)
{
try
{
var result = content.FromJson<keyvautlerrorresponse>();
var exception = new Exception($"{result.error.code} {result.error.message}");
if(result.error.innererror!=null)
{
var innerException = new Exception($"{result.error.innererror.code} {result.error.innererror.message}");
}
return exception;
}
catch(Exception e)
{
return e;
}
}
class keyvaultresponse
{
public string value { get; set; }
public string contentType { get; set; }
}
class keyvautlerrorresponse
{
public keyvaulterror error {get;set;}
}
class keyvaulterror
{
public string code { get; set; }
public string message { get; set; }
public keyvaulterror innererror { get; set; }
}
class AzureAuthenticator : IAuthenticator
{
private string _authority;
private string _clientId;
private string _clientSecret;
private string _resource;
public AzureAuthenticator(string resource)
{
_authority = WebConfigurationManager.AppSettings["azure:Authority"];
_clientId = WebConfigurationManager.AppSettings["azure:ClientId"];
_clientSecret = WebConfigurationManager.AppSettings["azure:ClientSecret"];
_resource = resource;
}
public AzureAuthenticator(string resource, string tennant, string clientid, string secret)
{
//https://login.microsoftonline.com/<tennant>/oauth2/oken
_authority = authority;
//azure client id (web app or native app
_clientId = clientid;
//azure client secret
_clientSecret = secret;
//vault.azure.net
_resource = resource;
}
public void Authenticate(IRestClient client, IRestRequest request)
{
var token = GetS2SAccessTokenForProdMSA().AccessToken;
//Trace.WriteLine(String.Format("obtained bearer token {0} from ADAL and adding to rest request",token));
request.AddHeader("Authorization", String.Format("Bearer {0}", token));
}
public AuthenticationResult GetS2SAccessTokenForProdMSA()
{
return GetS2SAccessToken(_authority, _resource, _clientId, _clientSecret);
}
private AuthenticationResult GetS2SAccessToken(string authority, string resource, string clientId, string clientSecret)
{
var clientCredential = new ClientCredential(clientId, clientSecret);
AuthenticationContext context = new AuthenticationContext(authority, false);
AuthenticationResult authenticationResult = context.AcquireToken(
resource,
clientCredential);
return authenticationResult;
}
}
}