我正在尝试在我的应用程序中使用 IHttpClientFactory,以便有效地管理在其整个生命周期中使用的 HTTP 客户端。
这些客户端中的每一个都需要根据特定的客户端 ID 设置其承载令牌,该客户端 ID 仅在运行时已知(在任何时候我们都可以使用多个客户端 ID),因此本质上这是我必须使用的参数获取授权标头。因为这些标头经常过期(我无法控制它何时会发生),所以我想重置令牌标头并在 401 状态响应上重试调用。
我尝试使用 HTTPClientService 创建它们,它通过依赖注入接收 IHTTPClientFactory:
internal class HttpClientService
{
private readonly IHttpClientFactory _httpClientFactory;
public HttpClientService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
//string should be the Namespace????
public async Task<HttpClient> GetHttpClientAsync(string clientId)
{
var client = _httpClientFactory.CreateClient("BasicClient");
//get token
client.DefaultRequestHeaders.Authorization = await GetBearerTokenHeader(clientId);
return client;
}
}
因为令牌经常过期(我无法控制它何时会发生),我想重置令牌标头并重试 401 状态响应的调用,所以我使用自定义 HttpMessageHandler 来执行此操作:
services.AddHttpClient("BasicClient").AddPolicyHandler(GetRetryPolicy()).AddHttpMessageHandler<TokenFreshnessHandler>();
TokenFreshnessHandler类:
public class TokenFreshnessHandler : DelegatingHandler
{
public TokenFreshnessHandler()
{
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode != HttpStatusCode.Unauthorized)
{
//REFRESH TOKEN AND SEND AGAIN!!!
request.Headers.Authorization = await GetBearerTokenHeader(/** CLIENT ID CANNOT BE RETRIEVED**/);
response = await base.SendAsync(request, cancellationToken);
}
return response;
}
}
但不幸的是,当我有机会重置令牌标头时,我无法知道原始的 ClientID 参数
是否有一种有效的方法可以让 HTTPRequestMessage 访问我的 ClientID?
我能否以某种方式使用我的 HTTPClient 将一些值传递到 HTTPRequest 消息的 Options 属性中?
好吧,当然有一些方法可以做到这一点,而无需更改请求。
但这完全取决于您将如何使用该
HttpClientService
以及它如何在DI
中注册。
因此,例如,您可以在
DI
中有一个“会话”对象,它被注册为单例,但该值被视为本地值,因此您可以使用工厂来获取它的最新版本。
public class ClientSession
{
private AsyncLocal<string> clientId { get; set; }
public string? ClientId
{
get => this.clientId?.Value;
set => this.clientId?.Value = value;
}
}
public class ClientSessionFactory : IClientSessionFactory
{
private readonly IServiceProvider serviceProvider;
public ClientSessionFactory(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public ClientSession Create()
{
return serviceProvider.GetRequiredService<ClientSession>();
}
}
所以,这个工厂可以注入到里面
HttpHandler
:
private readonly IClientSessionFactory ClientSessionFactory;
public TokenFreshnessHandler(IClientSessionFactory ClientSessionFactory)
{
this.ClientSessionFactory = ClientSessionFactory;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var clientSession = ClientSessionFactory.Create();
}
您可以在创建客户之前设置
client id
:
var clientSession = ClientSessionFactory.Create();
clientSession.ClientId = "xpto";
var httpClient = httpClientService.GetHttpClientAsync();
就是这个主意。
此外,您可以调整它以像这样使用:
using (var clientSession = ClientSessionFactory.Create())
{
clientSession.ClientId = "xpto";
var httpClient = httpClientService.GetHttpClientAsync();
}
但是你必须实施一些处置机制。
当将数据从一个
DelegatingHandler
传递到另一个关于HttpRequestMessage
的数据时,请使用Options
属性。
但是当将数据从一个 Polly 策略传递到另一个策略时(通过使用
PolicyWrap
或 HttpClient
上的链式策略包装),那么您应该使用 Polly 的 Context
功能。这可以通过方便的扩展方法来完成。
在您的主机设置代码中:
services.AddHttpClient<IHttpService, HttpService>()
.AddPolicyHandler(_ => Policy<HttpResponseMessage>.HandleResult(r => r.StatusCode == HttpStatusCode.Unauthorized)
.RetryAsync(1))
.AddHttpMessageHandler<AuthorizationMessageHandler>();
包装 HTTP 客户端的 HTTP 服务:
public class HttpService : IHttpService
{
private readonly HttpClient _httpClient;
public HttpService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<ApiResponse?> GetResource()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://myurl.com");
return await Get<ApiResponse>(request);
}
private async Task<TResponse?> Get<TResponse>(HttpRequestMessage request)
where TResponse : class
{
request.SetPolicyExecutionContext(new Context { ["clientId"] = "myClientId" });
var responseMessage = await _httpClient.SendAsync(request);
var response = await JsonSerializer.DeserializeAsync<TResponse>(await responseMessage.Content.ReadAsStreamAsync());
return response;
}
}
public interface IHttpService
{
Task<ApiResponse?> GetResource();
}
public class ApiResponse { }
您的
AuthorizationMessageHandler
确保始终在请求消息上设置令牌:
public class AuthorizationMessageHandler : DelegatingHandler
{
private readonly ITokenAcquisitionService _tokenAcquisitionService;
public AuthorizationMessageHandler(ITokenAcquisitionService tokenAcquisitionService)
{
_tokenAcquisitionService = tokenAcquisitionService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var context = request.GetPolicyExecutionContext();
var clientId = context?["clientId"] as string ?? throw new InvalidOperationException("No clientId found in execution context");
var token = await _tokenAcquisitionService.GetToken(clientId);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
获取授权令牌的服务接口:
public interface ITokenAcquisitionService
{
Task<string> GetToken(string clientId);
}