c# - .net services.AddHttpClient 自动访问令牌处理
问题描述
我正在尝试编写一个 Blazor 应用程序,该应用程序使用客户端机密凭据来获取 API 的访问令牌。我想以这样一种方式封装它,让它在后台处理令牌获取和刷新。为此,我创建了以下使用 IdentityModel Nuget 包的继承类:
public class MPSHttpClient : HttpClient
{
private readonly IConfiguration Configuration;
private readonly TokenProvider Tokens;
private readonly ILogger Logger;
public MPSHttpClient(IConfiguration configuration, TokenProvider tokens, ILogger logger)
{
Configuration = configuration;
Tokens = tokens;
Logger = logger;
}
public async Task<bool> RefreshTokens()
{
if (Tokens.RefreshToken == null)
return false;
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
if (disco.IsError) throw new Exception(disco.Error);
var result = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = Configuration["Settings:ClientID"],
RefreshToken = Tokens.RefreshToken
});
Logger.LogInformation("Refresh Token Result {0}", result.IsError);
if (result.IsError)
{
Logger.LogError("Error: {0)", result.ErrorDescription);
return false;
}
Tokens.RefreshToken = result.RefreshToken;
Tokens.AccessToken = result.AccessToken;
Logger.LogInformation("Access Token: {0}", result.AccessToken);
Logger.LogInformation("Refresh Token: {0}" , result.RefreshToken);
return true;
}
public async Task<bool> CheckTokens()
{
if (await RefreshTokens())
return true;
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
if (disco.IsError) throw new Exception(disco.Error);
var result = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = Configuration["Settings:ClientID"],
ClientSecret = Configuration["Settings:ClientSecret"]
});
if (result.IsError)
{
//Log("Error: " + result.Error);
return false;
}
Tokens.AccessToken = result.AccessToken;
Tokens.RefreshToken = result.RefreshToken;
return true;
}
public new async Task<HttpResponseMessage> GetAsync(string requestUri)
{
DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Tokens.AccessToken);
var response = await base.GetAsync(requestUri);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (await CheckTokens())
{
DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Tokens.AccessToken);
response = await base.GetAsync(requestUri);
}
}
return response;
}
}
这个想法是避免编写一堆冗余代码来尝试 API,然后在未经授权的情况下请求/刷新令牌。我一开始尝试使用 HttpClient 的扩展方法,但没有很好的方法将配置注入静态类。
所以我的服务代码是这样写的:
public interface IEngineListService
{
Task<IEnumerable<EngineList>> GetEngineList();
}
public class EngineListService : IEngineListService
{
private readonly MPSHttpClient _httpClient;
public EngineListService(MPSHttpClient httpClient)
{
_httpClient = httpClient;
}
async Task<IEnumerable<EngineList>> IEngineListService.GetEngineList()
{
return await JsonSerializer.DeserializeAsync<IEnumerable<EngineList>>
(await _httpClient.GetStreamAsync($"api/EngineLists"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
}
}
一切都编译得很好。在我的启动中,我有以下代码:
services.AddScoped<TokenProvider>();
services.AddHttpClient<IEngineListService, EngineListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
});
为了完整起见,Token Provider 看起来像这样:
public class TokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
当我运行应用程序时,它抱怨在对 services.AddHttpClient 的调用中找不到合适的 EngineListService 构造函数。有没有办法将 IEngineListService 的实际实例传递给 AddHttpClient。我可以通过其他任何方式实现这一目标吗?
谢谢,吉姆
解决方案
特别感谢 @pinkfloydx33 帮助我解决这个问题。他分享的这个链接https://blog.joaograssi.com/typed-httpclient-with-messagehandler-getting-accesstokens-from-identityserver/就是我需要的一切。诀窍是存在一个名为 DelegatingHandler 的类,您可以继承和覆盖 OnSendAsync 方法,并在将其发送到最终的 HttpHandler 之前在那里进行所有令牌检查。所以我的新 MPSHttpClient 类是这样的:
public class MPSHttpClient : DelegatingHandler
{
private readonly IConfiguration Configuration;
private readonly TokenProvider Tokens;
private readonly ILogger<MPSHttpClient> Logger;
private readonly HttpClient client;
public MPSHttpClient(HttpClient httpClient, IConfiguration configuration, TokenProvider tokens, ILogger<MPSHttpClient> logger)
{
Configuration = configuration;
Tokens = tokens;
Logger = logger;
client = httpClient;
}
public async Task<bool> CheckTokens()
{
var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
if (disco.IsError) throw new Exception(disco.Error);
var result = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = Configuration["Settings:ClientID"],
ClientSecret = Configuration["Settings:ClientSecret"]
});
if (result.IsError)
{
//Log("Error: " + result.Error);
return false;
}
Tokens.AccessToken = result.AccessToken;
Tokens.RefreshToken = result.RefreshToken;
return true;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.SetBearerToken(Tokens.AccessToken);
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (await CheckTokens())
{
request.SetBearerToken(Tokens.AccessToken);
response = await base.SendAsync(request, cancellationToken);
}
}
return response;
}
}
这里最大的变化是继承,我使用 DI 来获取 HttpClient ,就像@Rosco 提到的那样。我曾试图在我的原始版本中覆盖 OnGetAsync。从 DelegatingHandler 继承时,您只需覆盖 OnSendAsync。这将在一个方法中处理所有从 HttpContext 中获取、放置、发布和删除的操作。
我的 EngineList 服务写得好像没有要考虑的令牌,这是我最初的目标:
public interface IEngineListService
{
Task<IEnumerable<EngineList>> GetEngineList();
}
public class EngineListService : IEngineListService
{
private readonly HttpClient _httpClient;
public EngineListService(HttpClient httpClient)
{
_httpClient = httpClient;
}
async Task<IEnumerable<EngineList>> IEngineListService.GetEngineList()
{
return await JsonSerializer.DeserializeAsync<IEnumerable<EngineList>>
(await _httpClient.GetStreamAsync($"api/EngineLists"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
}
}
令牌提供者保持不变。我计划为其添加过期等,但它按原样工作:
public class TokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
ConfigureServices 代码只做了一点改动:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<TokenProvider>();
services.AddTransient<MPSHttpClient>();
services.AddHttpClient<IEngineListService, EngineListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
}).AddHttpMessageHandler<MPSHttpClient>();
...
}
您将 MPSHttpClient 实例化为 Transient,然后使用附加到 AddHttpClient 调用的 AddHttpMessageHandler 调用来引用它。我知道这与其他人实现 HttpClients 的方式不同,但我从 Pluralsight 视频中学习了这种创建客户端服务的方法,并且一直将其用于一切。我为数据库中的每个实体创建一个单独的服务。如果说我想做轮胎,我会在 ConfigureServices 中添加以下内容:
services.AddHttpClient<ITireListService, TireListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
}).AddHttpMessageHandler<MPSHttpClient>();
它将使用相同的 DelegatingHandler,因此我可以继续为每种实体类型添加服务,而不再担心令牌。感谢所有回复的人。
谢谢,吉姆