我发现这个答案非常不完整:
使用带类型客户端的Polly刷新令牌
与本文中的问题相同,只是公认的答案似乎存在严重缺陷。每次你提出请求时,都会收到401错误,然后你获得一个访问令牌,将其附加到请求中,然后重试。它是有效的,但您在每条消息上都会出错。
我看到的唯一解决方案是设置默认的身份验证标头,但要做到这一点,您需要HttpClient
。因此,最初的答案需要这样做:
services.AddHttpClient<TypedClient>()
.AddPolicyHandler((provider, request) =>
{
return Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.Unauthorized)
.RetryAsync(1, (response, retryCount, context) =>
{
var httpClient = provider.GetRequiredService<HttpClient>();
var authService = provider.GetRequiredService<AuthService>();
httpClient.DefaultRequestHeaders.Authorization = authService.GetAccessToken());
});
});
});
所以问题是,如何获得当前的HttpClient
来设置默认的访问令牌,这样就不会在每次发出请求时都调用这个处理程序?我们只想在令牌过期时调用此处理程序。
正如我在评论中所说,我已经创建了两个解决方案:
- 一个利用自定义异常触发刷新和重试的程序
- 另一个使用Polly的上下文来指示需要刷新
这两个解决方案都使用了命名客户端。所以,在这里我只关注需要更改为使用类型化客户端的部分。
好消息是,您只需要更改解决方案的一小部分。
重写基于自定义异常的解决方案
只有以下代码需要更改:
services.AddHttpClient("TestClient")
.AddPolicyHandler((provider, _) => GetTokenRefresher(provider))
.AddHttpMessageHandler<TokenFreshnessHandler>();
到此:
services.AddHttpClient<ITestClient, TestClient>
.AddPolicyHandler((provider, _) => GetTokenRefresher(provider))
.AddHttpMessageHandler<TokenFreshnessHandler>();
ITestClient
和TestClient
实体独立于解决方案的其余部分。
重写基于上下文的解决方案
只需要更改以下代码
services.AddHttpClient("TestClient")
.AddPolicyHandler((sp, request) => GetTokenRefresher(sp, request))
.AddHttpMessageHandler<TokenRetrievalHandler>()
到此:
services.AddHttpClient<ITestClient, TestClient>
.AddPolicyHandler((sp, request) => GetTokenRefresher(sp, request))
.AddHttpMessageHandler<TokenRetrievalHandler>()
所以问题是,如何获得当前的
HttpClient
来设置默认访问令牌,这样就不会在每次发出请求时都调用这个处理程序了?我们只想在令牌过期时调用此处理程序。
使用我提出的解决方案,您不需要访问HttpClient
来设置默认标头,因为当前活动的访问令牌存储在singleton类(TokenService
(中。在DelegatingHandler
(TokenFreshnessHandler
/TokenRetrievalHandler
(中,检索最新、最大的令牌,并将其设置在HttpRequestMessage
上。
Peter Csala的回答是一个很好的开始。然而,我发现"context"的生存期只有HttpRequestMessage那么长,基本上每个HttpClient函数调用(Put、Post、Get、Delete、Send(一次。也就是说,它不会在函数调用之间持续存在,所以我们不断地向缓存请求令牌。在MSAL中,这个函数并不是完全琐碎的,它涉及上下文切换。我们应该能够只使用存储的字符串,直到我们得到401错误,然后向MSAL缓存请求新的令牌是合理的。
我相信这是一个更高效的TokenRetrievalService处理程序版本:
public class TokenRetrievalHandler : DelegatingHandler
{
private readonly ITokenService tokenService;
public TokenRetrievalHandler(ITokenService tokenService)
{
this.tokenService = tokenService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.tokenService.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
这是一个更高效的令牌服务版本:
public class TokenService : ITokenService
{
public string AccessToken { get; set; }
public async Task RefreshAccessTokenAsync()
{
this.AccessToken = await <GetTokenFromPromptorCache>();
}
}
代币更新政策看起来要简单得多:
private static IAsyncPolicy<HttpResponseMessage> GetTokenRefresher(IServiceProvider serviceProvider, HttpRequestMessage httpRequestMessage)
{
return Policy<HttpResponseMessage>
.HandleResult(response => response.StatusCode == HttpStatusCode.Unauthorized)
.RetryAsync(async (handler, retry) =>
{
await serviceProvider.GetRequiredService<ITokenService>().RefreshAccessTokenAsync();
});
}
再次,在大部分工作中,支持Peter Csala。这是一个小调整,它实现了重用令牌原始版本的原始目标,而不会在每次调用时干扰(例如(MSAL缓存。