.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

[.NET大牛之路 005] .NET 的执行模型

ADO.NET和.NET的关系?

VS2022 安装.NET 3.5/.NET 4/.NET 4.5/.NET 4.5.1目标包的方法

.net core 3.0和.net5有什么区别

能说一下ADO.NET 和.NET,还有asp.NET的区别吗?