我有一个持久的 ETL 作业编排,可以从外部 API 下载数据。外部 API 的令牌存储在 Azure Key Vault 中。 编排触发了多个具有许多活动功能的子编排,这些功能需要将身份验证令牌添加到请求标头中。
如果我想避免不必要地调用 Azure Key Vault 来获取相同的令牌,那么将 httpClient 与 DelegatingHandler 结合使用的最佳实践是什么?
我将 IHttpClientFactory 注入到编排类中,然后在每个活动函数中注入 CreateClient。
我是否应该在 Orchestrator 函数中创建一个 http 客户端并将其作为参数传递给每个活动函数,从而实现一个委托处理程序来附加 auth 标头?
这是示例编排代码。在我的实现中,我有许多活动函数,并在每个函数中创建一个 httpclient。
public class ETLOrchestration
{
private readonly IHttpClientFactory _factory;
public ETLOrchestration(IHttpClientFactory factory)
{
_factory = factory;
}
[Function(nameof(ETLOrchestration))]
public static async Task RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
ILogger logger = context.CreateReplaySafeLogger(nameof(ETLOrchestration));
string token = await context.CallActivityAsync<string>(nameof(GetAccesToken));
if (string.IsNullOrEmpty(token))
{
throw new Exception("No access token found for ETL in the KeyVault.");
}
logger.LogInformation("Access token retrieved from KeyVault.");
// Should I be creating the http client here and passing it instead of the token?
await context.CallActivityAsync(nameof(ETLActivity), token);
}
[Function("ETLOrchestration_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger("ETLOrchestration_HttpStart");
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(ETLOrchestration));
return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
[Function(nameof(GetAccesToken))]
public async Task<string> GetAccesToken([ActivityTrigger] FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger(nameof(GetAccesToken));
string accessToken = string.Empty;
// Use the KeyVault to get the access token
string kvUri = "https://key-vault";
var kv_client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
try
{
Response<KeyVaultSecret> accessTokenTask = await kv_client.GetSecretAsync($"ETL-secret");
return accessTokenTask.Value.Value;
}
catch (RequestFailedException)
{
logger.LogCritical("No access token found for ETL in the KeyVault.");
return string.Empty;
}
}
[Function(nameof(ETLActivity))]
public async Task ETLActivity([ActivityTrigger] string accessToken, FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger(nameof(ETLActivity));
HttpClient http_client = _factory.CreateClient("SomeAPIClient");
http_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var httpResponse = await http_client.GetAsync($"some-api/data");
//...
}
}
在 Program.cs 中我有指定的 http 客户端:
services.AddHttpClient("SomeAPIClient", http_client =>
{
http_client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("BaseUrlSomeAPI");
});
使用下面的代码在 Durable Azure 函数中使用 DelegatingHandler 实现 HttpClient。
AuthTokenDelegatingHandler.cs:
public class AuthTokenDelegatingHandler : DelegatingHandler
{
private readonly IKeyVaultService _keyVaultService;
private string _cachedToken;
public AuthTokenDelegatingHandler(IKeyVaultService keyVaultService)
{
_keyVaultService = keyVaultService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(_cachedToken))
{
_cachedToken = await _keyVaultService.GetAccessTokenAsync();
}
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _cachedToken);
return await base.SendAsync(request, cancellationToken);
}
}
IKeyVaultService.cs:
public interface IKeyVaultService
{
Task<string> GetAccessTokenAsync();
}
public class KeyVaultService : IKeyVaultService
{
private readonly SecretClient _kvClient;
public KeyVaultService(string kvUri)
{
_kvClient = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
}
public async Task<string> GetAccessTokenAsync()
{
try
{
var secret = await _kvClient.GetSecretAsync("ETL-secret");
return secret.Value.Value;
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to retrieve access token from Key Vault.", ex);
}
}
}
程序.cs:
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices(services =>
{
services.AddSingleton<IKeyVaultService>(new KeyVaultService("https://<Keyvault_Name>.vault.azure.net/"));
services.AddTransient<AuthTokenDelegatingHandler>();
services.AddHttpClient("APIClient")
.AddHttpMessageHandler<AuthTokenDelegatingHandler>();
services.AddSingleton<Function1>();
}).Build();
host.Run();
函数.cs:
public class Function1
{
private readonly IHttpClientFactory _httpClientFactory;
public Function1(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
[Function(nameof(Function1))]
public async Task<List<string>> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
ILogger logger = context.CreateReplaySafeLogger(nameof(Function1));
logger.LogInformation("Saying hello.");
var outputs = new List<string>();
HttpClient httpClient = _httpClientFactory.CreateClient("SomeAPIClient");
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), new SayHelloInput { Name = "Tokyo" }));
return outputs;
}
[Function(nameof(SayHello))]
public static async Task<string> SayHello([ActivityTrigger] SayHelloInput input, FunctionContext executionContext)
{
string name = input.Name;
var factory = executionContext.InstanceServices.GetService<IHttpClientFactory>();
HttpClient httpClient = factory.CreateClient("SomeAPIClient");
ILogger logger = executionContext.GetLogger("SayHello");
logger.LogInformation("Saying hello to {name}.", name);
var response = await httpClient.GetAsync("https://www.google.com/");
return $"Hello {name}! Response: {response.StatusCode}";
}
[Function("Function1_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
[HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger("Function1_HttpStart");
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(Function1));
logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);
return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
public class SayHelloInput
{
public string Name { get; set; }
}
}
控制台输出:
Functions:
Function1_HttpStart: [GET,POST] http://localhost:7022/api/Function1_HttpStart
Function1: orchestrationTrigger
SayHello: activityTrigger
For detailed output, run func with --verbose flag.
[2025-01-08T13:45:55.409Z] Host lock lease acquired by instance ID '0000000000000000000000000D2022A4'.
[2025-01-08T13:45:56.341Z] Executing 'Functions.Function1_HttpStart' (Reason='This function was programmatically called via the host APIs.', Id=f182be81-0086-4ae9-b346-023889621020)
[2025-01-08T13:45:56.798Z] Scheduling new Function1 orchestration with instance ID 'e3a84bb6667f4ae499690d68a86b34d4' and 0 bytes of input data.
[2025-01-08T13:45:57.063Z] Started orchestration with ID = 'e3a84bb6667f4ae499690d68a86b34d4'.
[2025-01-08T13:45:57.160Z] Executed 'Functions.Function1_HttpStart' (Succeeded, Id=f182be81-0086-4ae9-b346-023889621020, Duration=844ms)
[2025-01-08T13:45:57.307Z] Executing 'Functions.Function1' (Reason='(null)', Id=fdae683a-aa44-4a8c-ac58-af50abdb34f9)
[2025-01-08T13:45:57.484Z] Saying hello.
[2025-01-08T13:45:57.538Z] Executed 'Functions.Function1' (Succeeded, Id=fdae683a-aa44-4a8c-ac58-af50abdb34f9, Duration=253ms)
[2025-01-08T13:45:57.679Z] Executing 'Functions.SayHello' (Reason='(null)', Id=9ee48e97-cea3-454c-89e9-95b096efac9d)
[2025-01-08T13:45:57.698Z] Saying hello to Tokyo.
[2025-01-08T13:45:57.710Z] Start processing HTTP request GET [https://www.google.com/](https://www.google.com/ "https://www.google.com/")
[2025-01-08T13:46:01.063Z] Sending HTTP request GET [https://www.google.com/](https://www.google.com/ "https://www.google.com/")
[2025-01-08T13:46:01.614Z] Received HTTP response headers after 542.4069ms - 200
[2025-01-08T13:46:01.617Z] End processing HTTP request after 3915.3171ms - 200
[2025-01-08T13:46:01.845Z] Executed 'Functions.SayHello' (Succeeded, Id=9ee48e97-cea3-454c-89e9-95b096efac9d, Duration=4191ms)
[2025-01-08T13:46:01.917Z] Executing 'Functions.Function1' (Reason='(null)', Id=dd783c6f-1470-4574-9d53-f433fe19a66d)
[2025-01-08T13:46:01.939Z] Executed 'Functions.Function1' (Succeeded, Id=dd783c6f-1470-4574-9d53-f433fe19a66d, Duration=23ms)
回复: