.net services.AddHttpClient 自动访问令牌处理
Posted
技术标签:
【中文标题】.net services.AddHttpClient 自动访问令牌处理【英文标题】:.net services.AddHttpClient Automatic Access Token Handling 【发布时间】:2021-07-07 21:01:17 【问题描述】:我正在尝试编写一个 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 的扩展方法,但是没有很好的方法将 Configuration 注入到静态类中。
所以我的服务代码是这样写的:
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。还有其他方法可以实现吗?
谢谢, 吉姆
【问题讨论】:
您认为使用继承原始 httpclient 的 httpclient 会赢得什么?使用组合而不是继承不是更容易吗,特别是因为 net core 有一个特殊的 httpclient 工厂? 通过使用new
,您并没有覆盖该方法。因此,当您有 HttpClient 引用时,它不会调用您的方法。而 AddHttpClient 只会添加普通的 HttpClient (不是子类)......但是!这正是 DelegatingHandler 的情况......它与 AddHttpClient 一起内置了支持
@Serge 你所说的组合而不是继承是什么意思?我想要赢得的是我可以正常调用 HttpHandler 的孩子,它会为我获取令牌。任何对 GetAsync() 的调用都会自动检查访问令牌并在需要时进行更新。
techwatching.dev/posts/delegating-handler 请参阅博客后面的部分,标题为“使用委托处理程序”或blog.joaograssi.com/…的全部内容
我还强烈建议您使用自定义委托处理程序,而不是从 HttpClient
派生。
【参考方案1】:
我认为EngineListService
不应该在服务中注册为HttpClient
,而应该注册MPSHttpClient
。
这遵循文档中的“类型化客户端”示例,并在幕后使用IHttpClientFactory
。
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#typed-clients
当您使用services.AddHttpClient
时,构造函数需要HttpClient
参数。这就是HttpClientFactory
初始化HttpClient
的方式,然后将其传递到您的服务中。
您可以将MPSHttpClient
更改为不继承HttpClient
,而是将HttpClient
参数添加到构造函数。你也可以让它实现一个接口,比如IMPSHttpClient
public class MPSHttpClient
public MPSHttpClient(HttpClient httpClient, IConfiguration configuration, TokenProvider tokens, ILogger logger)
HttpClient = httpClient;
Configuration = configuration;
Tokens = tokens;
Logger = logger;
您必须从MPSHttpClient
中删除这些行并使用注入的客户端。
// remove this
var client = new HttpClient();
在启动添加
services.AddHttpClient<MPSHttpClient>(client =>
// add any configuration
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
);
将EngineListService更改为普通服务注册,因为它不是HttpClient
services.AddScoped<IEngineListService, EngineListService>()
【讨论】:
我添加处理程序的方法是文档中的标准,只是我使用了 MPSHttpClient 而不是 HttpClient。在您的情况下,我仍然需要在启动时添加 IEngineListService 某处。有什么建议吗? 我认为既然使用了net core,也许注入httpClientFactory而不是httpClient更好?可以在构造函数内部创建的 Http 客户端 我已经添加了一个文档链接,以显示它在您调用 AddHttpClient() 并注册 IEngineListService 时使用 IHttpClientFactory【参考方案2】:特别感谢@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 Service 写得好像没有要考虑的令牌,这是我最初的目标:
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,因此我可以继续为每种实体类型添加服务,而不再担心令牌。感谢所有回复的人。
谢谢, 吉姆
【讨论】:
你还是有点落后。您不应该在委托处理程序中接受 httpclient 。 httpclient 使用处理程序(基本上它们组成了一个客户端)。您正确地完成了 DI 注册。但是在处理程序中,您可以访问发送异步,这就是您所需要的。我认为你最终会出现一个循环(即每个 http 调用都会调用处理程序两次) @pinkfloydx33 我明白你的意思,但这并没有发生。我设置了一些断点,并且在 CheckTokens 函数中的 HTTP 请求期间没有调用 SendAsync。我认为原因是我的处理程序只是为 EngineListService 连接,所以传递给我的处理程序的 HttpClient 没有连接通过我的处理程序。我需要一个 HttpClient,因为 IdentityModel 函数是 HttpClient 的扩展。以上是关于.net services.AddHttpClient 自动访问令牌处理的主要内容,如果未能解决你的问题,请参考以下文章
.NET平台系列26:在 Windows 上安装 .NET Core/.NET5/.NET6