У меня есть DelegatingHandler
, который передается в конструкторе синглтона HttpClient
.
Этот обработчик отвечает за выполнение базовой аутентификации для получения токена-носителя, который используется для последующих запросов досрок действия токена истекает.Когда срок действия токена истекает, базовая аутентификация запускается снова, чтобы обновить токен и т. Д.
public class MyMessageHandler : DelegatingHandler
{
private readonly string baseAddress;
private readonly string user;
private readonly string pass;
private readonly SemaphoreSlim sem;
private Token token;
public MyMessageHandler() : base()
{
// validation/assignment of baseAddress, userName, password
// ..omitted for brevity
sem = new SemaphoreSlim(1);
// this is the first time, so get the token
token = GetTokenAsync().GetAwaiter().GetResult();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!request.Headers.Contains("Authorization"))
{
request.Headers.Add("Authorization", $"Bearer {token.AccessToken}");
}
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
// if a token refresh is needed
if (response.StatusCode == HttpStatusCode.Unauthorized && whateverOtherCheckToTriggerTokenRefresh
{
try
{
// don't want multiple requests refreshing the token
await sem.WaitAsync().ConfigureAwait(false);
token = await GetTokenAsync().ConfigureAwait(false);
// we have the token, now set the headers to the new values
request.Headers.Remove("Authorization");
request.Headers.Add("Authorization", $"Bearer {token.AccessToken}");
// replay the request with the new token
response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
}
finally
{
sem.Release();
}
}
return response;
}
private async Task<Token> GetTokenAsync()
{
var authBytes = Encoding.UTF8.GetBytes($"{user}:{pass}");
var basicAuthToken = Convert.ToBase64String(authBytes);
var pairs = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "client_credentials")
};
// get ourselves a token using basic auth
var message = new HttpRequestMessage(HttpMethod.Post, new Uri(new Uri(baseAddress), "/token"))
{
Content = new FormUrlEncodedContent(pairs)
};
message.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuthToken);
var response = await base.SendAsync(message, new CancellationToken()).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// return our token
return JsonConvert.DeserializeObject<Token>(result);
}
}
Я использовал Semaphore
на этапе обновления / обновления токена, так как это одновременный WCFприложение, и я не хочу, чтобы несколько запросов все запрашивали новый токен.После завершения этапа обновления в поле token
для MyMessageHandler
устанавливается новый объект Token
, возвращаемый GetTokenAsync()
, и семафор освобождается, поэтому другие ожидающие запросы будут входить в блок кода.
1) Как теперь запретить зависшим запросам, ожидающим в строке await sem.WaitAsync().ConfigureAwait(false);
, выполнить шаг обновления токена?
2) Должен ли я беспокоиться о входящих запросах, пытающихся получить значение поля token
во время его обновления в семафоре?Если да, должен ли я сделать что-то вроде Interlocked.Exchange(ref token, newlyFetchedToken);
, как только будет получен новый токен?
Обновление :
После ответа Damien_The_Unbeliever, вот мой подход к реализацииего ответ.Мне все еще трудно понять, как реализовать ответ.
public class MyMessageHandler : DelegatingHandler
{
private readonly string baseAddress;
private readonly string user;
private readonly string pass;
private TaskToken> tokenTask;
public MyMessageHandler() : base()
{
// validation/assignment of baseAddress, userName, password
// ..omitted for brevity
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var localTokenTask = tokenTask;
Token localToken;
if (!request.Headers.Contains("Authorization"))
{
request.Headers.Add("Authorization", $"Bearer {localToken.AccessToken}");
}
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
// if a token refresh is needed
if (whateverOtherCheckToTriggerTokenRefresh)
{
var tcs = new TaskCompletionSource<Token>();
if (localTokenTask != Interlocked.CompareExchange(ref tokenTask, tcs.Task, localTokenTask))
{
// get latest value of Task<Token> field locally
localTokenTask = tokenTask;
localToken = await localTokenTask;
request.Headers.Authorization = new AuthenticationHeaderValue($"{localToken.TokenType}", $"{localToken.AccessToken}");
}
else
{
var newToken = await GetTokenAsync();
tcs.SetResult(newToken);
request.Headers.Authorization = new AuthenticationHeaderValue($"{newToken.TokenType}", $"{newToken.AccessToken}");
}
}
response = await base.SendAsync(request, cancellationToken);
return response;
}
private async Task<Token> GetTokenAsync()
{
var authBytes = Encoding.UTF8.GetBytes($"{user}:{pass}");
var basicAuthToken = Convert.ToBase64String(authBytes);
var pairs = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "client_credentials")
};
// get ourselves a token using basic auth
var message = new HttpRequestMessage(HttpMethod.Post, new Uri(new Uri(baseAddress), "/token"))
{
Content = new FormUrlEncodedContent(pairs)
};
message.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuthToken);
var response = await base.SendAsync(message, new CancellationToken()).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// return our token
return JsonConvert.DeserializeObject<Token>(result);
}
}