У меня есть немного проблем с обновлением токенов обновления в определенной ситуации, когда приложение на одной странице выполняет несколько вызовов API одновременно. У меня есть SPA, который имеет стек, который состоит из следующего.
Html / JS SPA -> Приложение MVC -> WebAPI
Я использую гибридный поток, когда пользователь входит на страницу, где я сохраняю id_token
, access_token
и refresh_token
в файле cookie сеанса.
Я использую HttpClient
, который имеет два DelegatingHandlers
, чтобы общаться с веб-API. Один из делегирующих обработчиков просто добавляет токен доступа в заголовок авторизации. Другой выполняется до этого и проверяет время жизни, оставшееся на токене доступа. Если токену доступа осталось ограниченное время, то refresh_token используется для получения новых учетных данных и их сохранения в моем сеансе.
Вот код для OidcTokenRefreshHandler.
public class OidcTokenRefreshHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly OidcTokenRefreshHandlerParams _handlerParams;
public OidcTokenRefreshHandler(IHttpContextAccessor httpContextAccessor, OidcTokenRefreshHandlerParams handlerParams)
{
_httpContextAccessor = httpContextAccessor;
_handlerParams = handlerParams;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
var handler = new JwtSecurityTokenHandler();
var accessTokenObj = handler.ReadJwtToken(accessToken);
var expiry = accessTokenObj.ValidTo;
if (expiry - TimeSpan.FromMinutes(_handlerParams.AccessTokenThresholdTimeInMinutes) < DateTime.UtcNow )
{
await RefreshTokenAsync(cancellationToken);
}
return await base.SendAsync(request, cancellationToken);
}
private async Task RefreshTokenAsync(CancellationToken cancellationToken)
{
var client = new HttpClient();
var discoveryResponse = await client.GetDiscoveryDocumentAsync(_handlerParams.OidcAuthorityUrl, cancellationToken);
if (discoveryResponse.IsError)
{
throw new Exception(discoveryResponse.Error);
}
var refreshToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = discoveryResponse.TokenEndpoint,
ClientId = _handlerParams.OidcClientId,
ClientSecret = _handlerParams.OidcClientSecret,
RefreshToken = refreshToken
}, cancellationToken);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
var tokens = new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.IdToken,
Value = tokenResponse.IdentityToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = tokenResponse.AccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = tokenResponse.RefreshToken
}
};
// Sign in the user with a new refresh_token and new access_token.
var info = await _httpContextAccessor.HttpContext.AuthenticateAsync("Cookies");
info.Properties.StoreTokens(tokens);
await _httpContextAccessor.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
}
}
Проблема в том, что многие звонки достигают этого примерно в одно и то же время. Все эти вызовы будут одновременно попадать в конечную точку обновления. Все они получат новые действительные токены доступа, и приложение продолжит работу. Однако, если 3 запроса происходят одновременно, будут созданы три новых токена обновления, и только один из них будет действительным. Из-за асинхронного характера приложения я не могу гарантировать, что токен обновления, сохраненный в моем сеансе, является фактически последним токеном обновления. В следующий раз, когда мне понадобится обновить токен обновления, он может быть недействительным (и часто это так).
Мои мысли о возможных решениях до сих пор.
Блокировка в точке проверки токена доступа с помощью мьютекса или подобного. Однако он может блокироваться, когда он используется другим пользователем с другим сеансом (насколько мне известно). Это также не работает, если мое приложение MVC установлено в нескольких экземплярах.
Измените, чтобы токены обновления оставались действительными после использования. Так что не имеет значения, кто из трех привыкнет.
Любые мысли о том, что из вышеперечисленного лучше или у кого-нибудь есть действительно умная альтернатива.
Большое спасибо!