在 Azure 持久编排中将 HttpClient 与 DelegatingHandler 结合使用

问题描述 投票:0回答:1

我有一个持久的 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");
});
c# .net azure-keyvault azure-durable-functions delegatinghandler
1个回答
0
投票

使用下面的代码在 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)

回复:

enter image description here

© www.soinside.com 2019 - 2024. All rights reserved.