如果 http 请求返回 401 错误,我将使用 polly 刷新授权令牌。我的完整解决方案基于:使用 Polly 和指定客户端刷新令牌,但相关部分是:
重试政策:
static IAsyncPolicy<HttpResponseMessage> GetAuthRefreshPolicy(IServiceProvider serviceProvider)
{
return Policy<HttpResponseMessage>
.HandleResult(response => response.StatusCode == HttpStatusCode.Unauthorized)
.RetryAsync(async (handler, retry) => await serviceProvider.GetRequiredService<IAuthorisationService>().RefreshTokenAsync());
}
授权服务:
public class AuthorisationService : IAuthorisationService
{
public String? AuthToken { get; private set; }
private readonly IAuthenticationClient _AuthenticationClient;
private readonly AuthOptions _AuthOptions;
public AuthorisationService(IAuthenticationClient authenticationClient, IOptions<AuthOptions> options)
{
_AuthenticationClient = authenticationClient;
_AuthOptions = options.Value;
}
public async Task RefreshTokenAsync()
{
this.AuthToken = await _AuthenticationClient.LoginAsync(_AuthOptions.User, _AuthOptions.Password);
}
}
这在大多数情况下都可以正常工作。我遇到的问题是,如果身份验证令牌当前无效并且同时发出多个请求,因为它们是异步执行的,它们都将使用无效令牌调用,失败并触发 polly 重试策略,从而导致多个不必要的请求令牌刷新请求。
有没有办法设置 polly,以便如果请求因 401 失败,所有后续请求都将等待,直到令牌成功刷新?或者对重试尝试进行排队并在已刷新的情况下跳过请求新令牌?
我尝试过研究诸如舱壁断路器之类的 Polly 功能,但无法确定这些功能是否适合此用例,或者是否有更简单的方法来锁定令牌刷新。
当我写下答案时,我认为很明显代码示例还没有生产就绪。
但是让我们关注您的具体问题。为了避免多个同时并发的令牌刷新调用,您必须保护
TokenService
的 RefreshToken
方法。这可以通过许多不同的方式来完成。让我向您展示如何使用 SemaphoreSlim
: 做到这一点
public class TokenService : ITokenService
{
private readonly SemaphoreSlim semaphore = new(1);
private DateTime lastRefreshed = DateTime.UtcNow;
public Token GetToken()
=> new() { Scheme = "Bearer", AccessToken = lastRefreshed.ToString("HH:mm:ss")};
public async Task RefreshToken()
{
await semaphore.WaitAsync();
try
{
//Use any arbitrary logic to detect simultaneous calls
if(lastRefreshed - DateTime.UtcNow < TimeSpan.FromSeconds(3))
{
Console.WriteLine("No refreshment happened");
return;
}
Console.WriteLine("Refreshment happened");
lastRefreshed = DateTime.UtcNow;
}
finally
{
semaphore.Release();
}
}
}
让我们测试一下代码
var service = new TokenService();
for (int i = 0; i < 10; i++)
{
Task.Run(async() =>
{
await Task.Delay(Random.Shared.Next() % 1000);
await service.RefreshToken();
});
}
输出将是:
Refreshment happened
No refreshment happened
No refreshment happened
No refreshment happened
No refreshment happened
No refreshment happened
No refreshment happened
No refreshment happened
No refreshment happened
No refreshment happened