Azure Durable编排中HttpClient结合DelegatingHandler避免重复取Token的最佳实践
Durable编排中结合HttpClient与DelegatingHandler避免重复调用Key Vault的最佳实践
提问背景
我有一个用于ETL作业的Durable编排,负责从外部API下载数据。外部API的认证Token存储在Azure Key Vault中。该编排会触发多个子编排及大量活动函数,这些函数都需要将认证Token添加到请求头中。
核心问题
- 若要避免不必要地重复调用Azure Key Vault获取相同Token,使用带有DelegatingHandler的HttpClient的最佳实践是什么?
- 我目前在编排类中注入了
IHttpClientFactory,然后在每个活动函数中调用CreateClient创建HttpClient。是否应该在编排器函数中创建一个HttpClient并将其作为参数传递给每个活动函数,同时实现DelegatingHandler来追加认证头?
示例代码
编排类代码
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("KeyVault中未找到ETL的访问令牌。"); } logger.LogInformation("已从KeyVault获取访问令牌。"); // 是否应该在这里创建HttpClient并传递给活动函数? 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; // 使用KeyVault获取访问令牌 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("KeyVault中未找到ETL的访问令牌。"); 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中的HttpClient配置
services.AddHttpClient("SomeAPIClient", http_client => { http_client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("BaseUrlSomeAPI")); });
最佳实践解答
首先明确:绝对不能将HttpClient传递给活动函数
Durable活动函数的参数必须是可序列化的,而HttpClient包含套接字、连接池等不可序列化的资源,直接传递会抛出序列化异常。此外,HttpClient的生命周期应由IHttpClientFactory管理,编排器属于长生命周期且可重播的组件,持有HttpClient会导致资源泄漏或意外行为。
正确的优化方案
1. 确保Token仅获取一次
你当前在编排器中调用一次GetAccesToken活动并将Token传递给后续活动的做法是正确的,继续保持即可。这样整个编排生命周期内只会调用一次Key Vault,完全避免重复获取Token的问题。
2. 用DelegatingHandler封装认证逻辑
创建自定义的DelegatingHandler,将添加认证头的逻辑封装起来,让活动函数的代码更简洁,也便于统一维护认证规则:
public class AuthenticationHeaderHandler : DelegatingHandler { private readonly string _bearerToken; public AuthenticationHeaderHandler(string bearerToken) { _bearerToken = bearerToken; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 避免重复添加Authorization头 if (!request.Headers.Contains("Authorization")) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _bearerToken); } return await base.SendAsync(request, cancellationToken); } }
然后在活动函数中,结合IHttpClientFactory创建带有该Handler的HttpClient:
[Function(nameof(ETLActivity))] public async Task ETLActivity([ActivityTrigger] string accessToken, FunctionContext executionContext) { ILogger logger = executionContext.GetLogger(nameof(ETLActivity)); // 获取工厂配置的基础客户端,复用BaseAddress等配置 var baseClient = _factory.CreateClient("SomeAPIClient"); // 绑定认证Handler到基础客户端的Handler链 var authHandler = new AuthenticationHeaderHandler(accessToken) { InnerHandler = baseClient.Handler }; // 创建带认证逻辑的HttpClient using var httpClient = new HttpClient(authHandler) { BaseAddress = baseClient.BaseAddress }; var httpResponse = await httpClient.GetAsync("some-api/data"); httpResponse.EnsureSuccessStatusCode(); // 处理响应逻辑... }
3. 优化SecretClient的使用
当前代码中手动创建SecretClient的方式不够优雅,建议通过依赖注入注册并注入,提升性能和可维护性:
修改Program.cs:
// 注册SecretClient string kvUri = "https://key-vault"; services.AddSecretClient(options => { options.VaultUri = new Uri(kvUri); }); // 保留原有的HttpClient注册 services.AddHttpClient("SomeAPIClient", http_client => { http_client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("BaseUrlSomeAPI")); });
修改GetAccesToken活动:
[Function(nameof(GetAccesToken))] public async Task<string> GetAccesToken([ActivityTrigger] FunctionContext executionContext, ISecretClient kvClient) { ILogger logger = executionContext.GetLogger(nameof(GetAccesToken)); try { var accessTokenSecret = await kvClient.GetSecretAsync("ETL-secret"); return accessTokenSecret.Value.Value; } catch (RequestFailedException) { logger.LogCritical("KeyVault中未找到ETL的访问令牌。"); return string.Empty; } }
内容的提问来源于stack exchange,提问作者Dorian-B
相关产品推荐
相关产品推荐

