首页 > 解决方案 > .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。我可以通过其他任何方式实现这一目标吗?

谢谢,吉姆

标签: c#.netblazordotnet-httpclient

解决方案


特别感谢 @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,因此我可以继续为每种实体类型添加服务,而不再担心令牌。感谢所有回复的人。

谢谢,吉姆


推荐阅读