如何使用IHttpClientFactory刷新token

35

我正在使用IHttpClientFactory通过Net Core 2.2向两个外部API发送请求并接收HTTP响应。

我正在寻找一种好的策略,使用存储在appsettings.json中的刷新令牌来获取新的访问令牌。当当前请求返回403或401错误时,需要请求新的访问令牌。当获取到新的访问和刷新令牌后,需要更新appsettings.json中的值以便在后续请求中使用。

我使用两个客户端发送请求到两个不同的API,但只有一个使用令牌身份验证机制。

我已经实现了一些简单的东西,但是我正在寻找一种更优雅的解决方案,可以在当前令牌过期时动态更新标头:

我已在Startup.ConfigureServices方法中注册了IHttpClientFactory,如下所示:

services.AddHttpClient();

一旦注册,我将使用它来调用两个不同的API,第一种方法是:

   public async Task<AirCallRequest> GetInformationAsync(AirCallModel model)
    {
        try
        {


            CandidateResults modelCandidateResult = null;

            var request = new HttpRequestMessage(HttpMethod.Get,
            "https://*******/v2/*****");
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _appSettings.Value.Token);


            var clientJAAPI = _httpClientFactory.CreateClient();
            var responseclientJAAPI = await clientJAAPI.SendAsync(request);


            if (responseclientJAAPI.IsSuccessStatusCode)
            {
                modelCandidateResult = await responseclientJAAPI.Content
                   .ReadAsAsync<CandidateResults>();

                ....
            }


            if ((responseclientJAAPI .StatusCode.ToString() == "Unauthorized")
            {                    

                await RefreshAccessToken();

               //Calls recursively this method again
                return await GetInformationAsync(model);

            }

            return null;
        }
        catch (Exception e)
        {
            return null;

        }

    }

刷新令牌的方法如下:

private async Task RefreshAccessToken()
    {


        var valuesRequest = new List<KeyValuePair<string, string>>();
        valuesRequest.Add(new KeyValuePair<string, string>("client_id", "*****"));
        valuesRequest.Add(new KeyValuePair<string, string>("client_secret","****"));
        valuesRequest.Add(new KeyValuePair<string, string>("grant_type", "refresh_token"));
        valuesRequest.Add(new KeyValuePair<string, string>("refresh_token", "*****"));


        RefreshTokenResponse refreshTokenResponse = null;

        var request = new HttpRequestMessage(HttpMethod.Post,
        "https://*****/connect/token");

        request.Content = new FormUrlEncodedContent(valuesRequest);

        var clientJAAPI = _httpClientFactory.CreateClient();
        var responseclientJAAPI = await clientJAAPI.SendAsync(request);

        if (responseclientJAAPI.IsSuccessStatusCode)
        {
            refreshTokenResponse = await responseclientJAAPI.Content.ReadAsAsync<RefreshTokenResponse>();

            //this updates the POCO object representing the configuration but not the appsettings.json :
            _appSettings.Value.Token = refreshTokenResponse.access_token;

        }

    }
注意,我正在更新表示配置的POCO对象,而不是appsettings.json,因此新值存储在内存中。我想更新appsettings.json以供后续请求使用。
如果提出的解决方案需要在Startup.ConfigureService中定义Httpclient的主要设置,则需要允许创建不同实例的HttpClien,因为其中一个HttpClient实例(用于调用第二个API的另一个方法)不需要令牌来发送请求。
2个回答

69

看起来你需要DelegatingHandler。简单来说,你可以“拦截”你的HTTP请求并添加授权头信息,然后尝试执行它,如果令牌无效,则刷新令牌并再次重试一次。就像这样:

public class AuthenticationDelegatingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await GetTokenAsync();
        request.Headers.Authorization = new AuthenticationHeaderValue(token.Scheme, token.AccessToken);
        var response = await base.SendAsync(request, cancellationToken);

        if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
        {
            token = await RefreshTokenAsync();
            request.Headers.Authorization = new AuthenticationHeaderValue(token.Scheme, token.AccessToken);
            response = await base.SendAsync(request, cancellationToken);
        }

        return response;
    }
}

您可以在 Startup.cs 中注册此委托处理程序,如下所示:

services.AddTransient<AuthenticationDelegatingHandler>();
services.AddHttpClient("MySecuredClient", client =>
    {
        client.BaseAddress = new Uri("https://baseUrl.com/");
    })
    .AddHttpMessageHandler<AuthenticationDelegatingHandler>();

就像这样使用:

var securedClient = _httpClientFactory.CreateClient("MySecuredClient");
securedClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "v2/relativeUrl"));

关于在appsetting.json中存储刷新令牌的问题,我认为这不是一个好主意,因为刷新令牌没有过期时间。如果您可以使用凭据获取新令牌,首次使用它,然后将刷新令牌存储在内存中以进行进一步的刷新。

这里您可以看到我如何管理客户端凭据令牌的刷新,并尝试使其适用于您的情况。


更新:

在这里,您可以找到同样的想法,但由专业人员实现,并且可以在nuget中获得。使用非常简单:

services.AddAccessTokenManagement(options =>
{
    options.Client.Clients.Add("identityserver", new ClientCredentialsTokenRequest
    {
        Address = "https://demo.identityserver.io/connect/token",
        ClientId = "m2m.short",
        ClientSecret = "secret",
        Scope = "api" // optional
    });
});

services.AddHttpClient<MyClient>(client =>
{
    client.BaseAddress = new Uri("https://demo.identityserver.io/api/");
})
.AddClientAccessTokenHandler();

MyClient 发送的请求将始终具有有效的令牌,刷新将自动执行。


1
我正在尝试实现类似于您的缓存方法,我正在使用clientId,并且该clientId是发送到API控制器的参数,该Action方法是使用SendAsync的方法。想知道如何在DelegatingHandler中读取该参数,知道DelegatingHandler是在启动类中实例化的。我为此创建了另一个问题,如果您想看一下:https://stackoverflow.com/questions/56456954/how-to-read-parameters-sent-to-an-action-method-webapi-within-a-delegatinghand - D.B
1
@Terry 我从未使用过这个库。但是查看了源代码,它默认添加了一个内存缓存实现。在我看来,在无服务器场景下只使用远程缓存是有道理的。在简单的场景中,每个服务实例都可以获取自己的令牌,将其存储在内存中,并避免在每次检索令牌时产生远程调用的开销。 - Artur
1
@Terry 我从未使用过这个库。但是查看了源代码,它默认添加了一个内存缓存实现。在我看来,在无服务器场景下使用远程缓存是有意义的。在简单的场景中,每个服务实例都可以获取自己的令牌,将其存储在内存中,并避免在每次令牌检索时产生远程调用的开销。 - undefined
1
顺便说一句,该库已经迁移到这里:https://github.com/DuendeSoftware/Duende.AccessTokenManagement/wiki - Artur
1
顺便说一句,该库已经迁移到这里:https://github.com/DuendeSoftware/Duende.AccessTokenManagement/wiki - undefined
显示剩余8条评论

3
我喜欢Artur提出的DelegatingHandler的想法。但是那个解决方案有代码重复的问题:
request.Headers.Authorization = new AuthenticationHeaderValue(token.Scheme, token.AccessToken);
response = await base.SendAsync(request, cancellationToken);

此处是在讨论代码中的 GetTokenAsyncRefreshTokenAsync 方法属于 DelegatingHandler 类的一部分,这可能不是最优的设计。
如果您想避免这些问题,可以使用Polly的重试策略DelegatingHandler和令牌管理服务的组合。
下面是描述通信流程的序列图。

refresh token in case of 401

相关的示例代码已发布在这里
更新 #1
另一种版本(将责任分开得更好)在这里可用。

refreshing token


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接