Я использую библиотеку Blazored.LocalStorage для хранения токенов JWT, возвращаемых методом входа в API. Затем я снова использую его при каждом другом вызове API, чтобы получить его и отправить в качестве токена на предъявителя. Я перевожу свои ранее функциональные жестко закодированные HTTP-клиенты на использование библиотеки Refit, а также DelegatingHandler для добавления токена JWT в заголовок авторизации каждого запроса. Обработчик выглядит следующим образом:
internal sealed class AuthenticationHandler(ILocalStorageService storageService) : DelegatingHandler
{
private readonly ILocalStorageService _storageService = storageService;
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var authToken = await _storageService.GetItemAsStringAsync("test", cancellationToken);
if (!string.IsNullOrEmpty(authToken))
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", authToken);
return await base.SendAsync(request, cancellationToken);
}
}
Вот как я регистрирую своих клиентов Refit:
builder.Services.AddRefitClient<ISomeRefitClientInterface>(refitSettings)
.ConfigureHttpClient(options =>
{
options.BaseAddress = new Uri(config["APIs:MainAPI"]!);
options.Timeout = TimeSpan.FromMinutes(5);
})
.AddHttpMessageHandler<AuthenticationHandler>()
.AddHttpMessageHandler<LoggingHandler>()
.AddHttpMessageHandler<RetryHandler>();
Два последних обработчика работают отлично, но AuthenticationHandler не хочет иметь ничего общего с локальным хранилищем, поскольку выдает следующее исключение: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendererd. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.
Предварительный рендеринг отключен глобально во всем моем приложении, а локальное хранилище прекрасно работает в методе OnInitializedAsync любого компонента (чего не было бы, если бы предварительный рендеринг был включен). До использования Refit я жестко запрограммировал свои HTTP-клиенты, и аутентификация выполнялась таким же образом, но без использования DelegatingHandler. Каждый клиент унаследован от общего базового класса, и логика добавления токена в заголовок аутентификации была там и вызывалась вручную для каждого запроса.
Быстрый поиск в Google обнаруживает аналогичные проблемы, о которых сообщалось на GitHub, но я не вижу решения. Кажется, что библиотеки, использующие взаимодействие Javascript, просто не работают изнутри DelegatingHandlers, поскольку я пробовал Blazored.SessionStorage с идентичными результатами.
Есть ли способ получить доступ к локальному хранилищу из DelegatingHandler?
По моему опыту, даже отключив предварительный рендерер (SSR) во всем мире, некоторые места все еще каким-то образом используют SSR. Единственный способ получить доступ к значению во всех режимах рендеринга — это cookie.
Вы можете использовать httpcontext.request/response.cookie в режиме SSR. И используйте JSinterop для управления файлами cookie браузера в режиме сервера Interacrive/WASM.
Сначала установите Microsoft.AspNetCore.Http 2.2.2
в клиентский проект.
Затем добавьте следующий скрипт внизу «App.razor».
<script>
function getCookie(key) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${key}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
function setCookie(key, value) {
document.cookie = `${key}=${value}`;
}
</script>
Затем вы можете определить режим рендеринга по httpcontext для работы с файлом cookie.
@using Microsoft.AspNetCore.Http
...
if (httpContext != null) //when SSR httpContext is not null.
{
//get cookie
var cookieValue = httpContext.Request.Cookies["key1"];
//set cookie
httpContext.Response.Cookies.Append("key1","value1");
}
else // when InterActive Server/WASM , httpContext is null. So use javascript interop.
{
//get cookkie
var cookieValue =await _JSruntime.InvokeAsync<string>("getCookie","key1");
//set cookie
await _JSruntime.InvokeVoidAsync("setCookie", "key1", "value1");
}
Я НАКОНЕЦ-ТО понял это! Оказывается, вы можете получить вошедшего в систему пользователя (и токен аутентификации, если вы сопоставили его с каким-либо утверждением) непосредственно из AuthenticationStateProvider Blazor из DelegatingHandler. Чтобы заставить DI правильно работать с IHttpClientFactory, требуется несколько наворотов, но все это подробно описано здесь: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/additional- сценарии?view=aspnetcore-8.0&preserve-view=true#access-authenticationstateprovider-in-outgoing-request-middleware
Я использовал аналогичный подход для аутентификации запросов API из моего приложения Blazor Wasm, хотя я не использую Refit. Я не писал свой собственный DelegatingHandler, а использовал AuthorizationMessageHandler, предоставленный Microsoft.
Мой код выглядит следующим образом:
var backendUrl = new Uri(builder.Configuration["BackendUrl"]);
builder.Services.AddScoped<AuthorizationMessageHandler>();
builder.Services
.AddHttpClient<ApiClient>(options =>
{
options.BaseAddress = backendUrl;
options.DefaultRequestVersion = HttpVersion.Version20;
options.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
})
.AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
.ConfigureHandler(authorizedUrls: new[] { backendUrl.AbsoluteUri }));
Затем вам также необходимо зарегистрировать IAccessTokenProvider следующим образом.
builder.Services.AddSingleton<IAccessTokenProvider, AccessTokenAuthenticationStateProvider>();
И в свой собственный AccessTokenAuthenticationStateProvider я добавляю Blazored ILocalStorageService, который здесь работает нормально.
К сожалению, этого не существует на сервере Blazor, который я использую.