You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Azure Durable编排中HttpClient结合DelegatingHandler避免重复取Token的最佳实践

Durable编排中结合HttpClient与DelegatingHandler避免重复调用Key Vault的最佳实践

提问背景

我有一个用于ETL作业的Durable编排,负责从外部API下载数据。外部API的认证Token存储在Azure Key Vault中。该编排会触发多个子编排及大量活动函数,这些函数都需要将认证Token添加到请求头中。

核心问题

  1. 若要避免不必要地重复调用Azure Key Vault获取相同Token,使用带有DelegatingHandler的HttpClient的最佳实践是什么?
  2. 我目前在编排类中注入了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 04:24:50