.net services.AddHttpClient 自动访问令牌处理
.net services.AddHttpClient Automatic Access Token Handling
我正在尝试编写一个 Blazor 应用程序,它使用客户端机密凭据来获取 API 的访问令牌。我想以这样一种方式封装它,让它在幕后处理令牌获取和刷新。为此,我创建了以下使用 IdentityModel Nuget 包的继承 class:
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,然后 request/refresh 如果您未经授权则尝试令牌。我首先尝试使用 HttpClient 的扩展方法,但是没有好的方法将配置注入静态 class.
所以我的服务代码是这样写的:
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 });
}
}
一切都编译得很好。在我的 Startup 中,我有以下代码:
services.AddScoped<TokenProvider>();
services.AddHttpClient<IEngineListService, EngineListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
});
为了完整起见,令牌提供程序如下所示:
public class TokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
当我运行 App 时,它抱怨说在services.AddHttpClient 的调用中找不到适合EngineListService 的构造函数。有没有办法将 IEngineListService 的实际实例传递给 AddHttpClient。还有其他方法可以实现吗?
谢谢,
吉姆
我认为 EngineListService
不应在服务中注册为 HttpClient
,您应该注册 MPSHttpClient
.
这遵循文档中的“Typed Client”示例并在幕后使用 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>()
特别感谢@pinkfloydx33 帮我解决了这个问题。他分享 https://blog.joaograssi.com/typed-httpclient-with-messagehandler-getting-accesstokens-from-identityserver/ 的这个 link 是我需要的一切。诀窍是存在一个名为 DelegatingHandler 的 class,您可以继承并覆盖 OnSendAsync 方法,并在将其发送到最终的 HttpHandler 之前在那里进行所有令牌检查。所以我的新 MPSHttpClient class 是这样的:
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 中获取、放置、post 和删除的操作。
我的 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,因此我可以继续为每个实体类型添加服务,而不再担心令牌。感谢所有回复的人。
谢谢,
吉姆
我正在尝试编写一个 Blazor 应用程序,它使用客户端机密凭据来获取 API 的访问令牌。我想以这样一种方式封装它,让它在幕后处理令牌获取和刷新。为此,我创建了以下使用 IdentityModel Nuget 包的继承 class:
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,然后 request/refresh 如果您未经授权则尝试令牌。我首先尝试使用 HttpClient 的扩展方法,但是没有好的方法将配置注入静态 class.
所以我的服务代码是这样写的:
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 });
}
}
一切都编译得很好。在我的 Startup 中,我有以下代码:
services.AddScoped<TokenProvider>();
services.AddHttpClient<IEngineListService, EngineListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
});
为了完整起见,令牌提供程序如下所示:
public class TokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
当我运行 App 时,它抱怨说在services.AddHttpClient 的调用中找不到适合EngineListService 的构造函数。有没有办法将 IEngineListService 的实际实例传递给 AddHttpClient。还有其他方法可以实现吗?
谢谢, 吉姆
我认为 EngineListService
不应在服务中注册为 HttpClient
,您应该注册 MPSHttpClient
.
这遵循文档中的“Typed Client”示例并在幕后使用 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>()
特别感谢@pinkfloydx33 帮我解决了这个问题。他分享 https://blog.joaograssi.com/typed-httpclient-with-messagehandler-getting-accesstokens-from-identityserver/ 的这个 link 是我需要的一切。诀窍是存在一个名为 DelegatingHandler 的 class,您可以继承并覆盖 OnSendAsync 方法,并在将其发送到最终的 HttpHandler 之前在那里进行所有令牌检查。所以我的新 MPSHttpClient class 是这样的:
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 中获取、放置、post 和删除的操作。
我的 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,因此我可以继续为每个实体类型添加服务,而不再担心令牌。感谢所有回复的人。
谢谢, 吉姆