/
va1eriy
/
taskwards
Обзор
Документация
Войти
/
va1eriy
/
taskwards
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Taskwards.Web/Infrastructure/Auth/AuthDelegatingHandler.cs
165 строк
6 KB
Valeriy Kokorev
feat: add automatic token refresh with AuthDelegatingHandler
27 фев 2026, 08:52
27 фев 2026, 08:52
afe7bb4
Код
Авторство
О чём код?
using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; namespace Taskwards.Web.Infrastructure.Auth; /// <summary> /// HTTP-обработчик для автоматического прикрепления JWT токенов и обновления истёкших access токенов /// </summary> public class AuthDelegatingHandler : DelegatingHandler { private static readonly SemaphoreSlim RefreshSemaphore = new(1, 1); private static readonly HashSet<string> AnonymousSegments = new(StringComparer.OrdinalIgnoreCase) { "login", "register", "refresh", "forgot-password", "reset-password", "confirm-email" }; /// <summary> /// Буфер в секундах — обновляем токен заранее, до фактического истечения /// </summary> private const int ExpirationBufferSeconds = 30; private readonly ITokenStorageService _tokenStorage; private readonly TaskwardsAuthStateProvider _authStateProvider; private readonly IServiceProvider _serviceProvider; public AuthDelegatingHandler( ITokenStorageService tokenStorage, TaskwardsAuthStateProvider authStateProvider, IServiceProvider serviceProvider) { _tokenStorage = tokenStorage; _authStateProvider = authStateProvider; _serviceProvider = serviceProvider; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (IsAnonymousEndpoint(request.RequestUri)) return await base.SendAsync(request, cancellationToken); var token = await GetOrRefreshTokenAsync(cancellationToken); if (!string.IsNullOrEmpty(token)) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await base.SendAsync(request, cancellationToken); if (response.StatusCode == HttpStatusCode.Unauthorized) { var newToken = await ForceRefreshTokenAsync(cancellationToken); if (!string.IsNullOrEmpty(newToken)) { var retryRequest = await CloneHttpRequestMessageAsync(request); retryRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken); response.Dispose(); return await base.SendAsync(retryRequest, cancellationToken); } } return response; } private async Task<string?> GetOrRefreshTokenAsync(CancellationToken cancellationToken) { var expiration = await _tokenStorage.GetAccessTokenExpirationAsync(); if (expiration.HasValue && expiration.Value.AddSeconds(-ExpirationBufferSeconds) <= DateTime.UtcNow) return await ForceRefreshTokenAsync(cancellationToken); return await _tokenStorage.GetAccessTokenAsync(); } private async Task<string?> ForceRefreshTokenAsync(CancellationToken cancellationToken) { await RefreshSemaphore.WaitAsync(cancellationToken); try { // После получения семафора — проверить, не обновил ли уже другой поток var expiration = await _tokenStorage.GetAccessTokenExpirationAsync(); if (expiration.HasValue && expiration.Value.AddSeconds(-ExpirationBufferSeconds) > DateTime.UtcNow) return await _tokenStorage.GetAccessTokenAsync(); var refreshToken = await _tokenStorage.GetRefreshTokenAsync(); if (string.IsNullOrEmpty(refreshToken)) { await ClearAndNotifyAsync(); return null; } try { var factory = _serviceProvider.GetRequiredService<IHttpClientFactory>(); using var refreshClient = factory.CreateClient("ApiClientNoAuth"); var response = await refreshClient.PostAsJsonAsync( "api/auth/refresh", new { refreshToken }, cancellationToken); if (!response.IsSuccessStatusCode) { await ClearAndNotifyAsync(); return null; } var apiResult = await response.Content.ReadFromJsonAsync<ApiAuthResponse>(cancellationToken); if (apiResult == null) { await ClearAndNotifyAsync(); return null; } var authResult = apiResult.ToAuthResult(); await _tokenStorage.SetTokensAsync(authResult); _authStateProvider.NotifyAuthStateChanged(); return authResult.Tokens.AccessToken; } catch { await ClearAndNotifyAsync(); return null; } } finally { RefreshSemaphore.Release(); } } private async Task ClearAndNotifyAsync() { await _tokenStorage.ClearTokensAsync(); _authStateProvider.NotifyAuthStateChanged(); } private static bool IsAnonymousEndpoint(Uri? uri) { if (uri == null) return false; var lastSegment = uri.Segments.LastOrDefault()?.TrimEnd('/'); return !string.IsNullOrEmpty(lastSegment) && AnonymousSegments.Contains(lastSegment); } private static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage request) { var clone = new HttpRequestMessage(request.Method, request.RequestUri); if (request.Content != null) { var content = await request.Content.ReadAsByteArrayAsync(); clone.Content = new ByteArrayContent(content); foreach (var header in request.Content.Headers) clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); } foreach (var header in request.Headers) clone.Headers.TryAddWithoutValidation(header.Key, header.Value); clone.Version = request.Version; return clone; } }