- Во-первых, интерфейс, написанный на Blazor Server, который выполняет некоторый рендеринг 3D-ресурсов. Это делается с помощью WebGL и некоторых подобных технологий. Где сервер выполняет некоторое кэширование, но в основном просто позволяет нам выполнять некоторую многопоточность, которую мы не можем выполнить с помощью Blazor WASM.
- Во-вторых, внутренний веб-API ASP.NET Core, который обеспечивает плоскость управления для наших служб, специфичных для приложения, и обрабатывает нашу внепроцессную обработку через некоторые микросервисы. Этот API является одновременно API REST и API gRPC, где gRPC в основном является просто прокси-сервером для других служб, требующих gRPC, но теперь с дополнительной аутентификацией.
- В-третьих, наша служба аутентификации, которая представляет собой экземпляр keycloak, работающий в кластере с другими службами.
Проблема
Большая часть этого работает так, как задумано; однако мне нужно иметь возможность выполнять вызовы gRPC способом, аутентифицированным для API.
В настоящее время я получаю ошибки, связанные с тем, что токен недоступен, поскольку HttpContext больше не существует к тому времени, когда должен произойти вызов gRPC. Я решил эту проблему на стороне REST API, используя DelegatingHandler в HttpClient, который используется для совершения вызовов. Однако при использовании GrpcWebHandler и HttpClientHandler не существует способа сделать то же самое с DelegatingHandler, поэтому мы создали оболочку вокруг GrpcChannel, позволяющую передавать токен через перехватчик. Похоже, это не работает.
После того, как пользователь вошел в систему и получил токен, мы вызываем API. Что завершится ошибкой http 401, поскольку запрос не содержит токена.
В частности:
Grpc.Core.RpcException: Status(StatusCode="Unauthenticated", Detail="Bad gRPC response. Код состояния HTTP: 401")
at Service.Grpc.GrpcClient
Код: Выделить всё
1.ReadResponse[TResponse](FuncКанал перехватчика выглядит так:
Код: Выделить всё
internal class AuthenticatedChannel(
GrpcChannel innerChannel,
IHttpContextAccessor httpContextAccessor,
string? initialToken,
ILogger logger)
: ChannelBase(innerChannel.Target)
{
public override CallInvoker CreateCallInvoker()
{
var callInvoker = innerChannel.CreateCallInvoker();
return callInvoker.Intercept(new WebAuthenticationInterceptor(httpContextAccessor, initialToken, logger));
}
}
Код: Выделить всё
internal class WebAuthenticationInterceptor(
IHttpContextAccessor httpContextAccessor,
string? fallbackToken) : Interceptor
{
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
private async Task GetAccessTokenAsync()
{
if (_httpContextAccessor.HttpContext is null) return fallbackToken;
var token = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
return !string.IsNullOrWhiteSpace(token) ? token : fallbackToken;
}
private Metadata AddAuthorizationHeader(Metadata? headers, string? accessToken)
{
var metadata = headers ?? new Metadata();
if (!string.IsNullOrWhiteSpace(accessToken))
{
metadata.Add("Authorization", $"Bearer {accessToken}");
}
else
{
_logger.LogWarning("No access token available for gRPC call");enter code here
}
return metadata;
}
//... other methods omitted for brevity
}
Код: Выделить всё
public class ChannelFactory : IChannelFactory
{
public ChannelFactory(
IConfiguration configuration,
IHttpContextAccessor httpContextAccessor,
ILogger logger)
{
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
if (httpContextAccessor.HttpContext is not null)
{
_circuitToken = httpContextAccessor.HttpContext.GetTokenAsync("access_token")
.GetAwaiter().GetResult();
}
if (string.IsNullOrWhiteSpace(_circuitToken))
{
_logger.LogWarning("No access token captured");
}
}
public async Task CreateChannel(
string target,
bool useExistingChannel,
CancellationToken cancellationToken)
{
string? token = null;
if (_httpContextAccessor.HttpContext is not null)
{
token = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
if (string.IsNullOrWhiteSpace(token))
{
_logger.LogWarning("Failed to acquire access token from HttpContext");
}
}
token ??= _circuitToken;
if (useExistingChannel && _channels.TryGetValue(target, out var existingChannel))
{
return new WebGrpcChannelWrapper(existingChannel, _httpContextAccessor, token, _logger);
}
var httpHandler = new GrpcWebHandler(new HttpClientHandler());
var channelOptions = new GrpcChannelOptions
{
HttpHandler = httpHandler,
MaxReceiveMessageSize = null,
MaxSendMessageSize = null
};
var channel = GrpcChannel.ForAddress(_apiBaseUrl, channelOptions);
if (useExistingChannel)
{
_channels[target] = channel;
}
return new WebGrpcChannelWrapper(channel, _httpContextAccessor, token, _logger);
}
}
Код: Выделить всё
public class ApiTokenHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _contextAccessor;
private readonly ILogger _logger;
public ApiTokenHandler(
IHttpContextAccessor contextAccessor,
ILogger logger)
{
_contextAccessor = contextAccessor;
_logger = logger;
}
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string? token = null;
if (_contextAccessor.HttpContext is not null)
{
token = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
}
if (!string.IsNullOrWhiteSpace(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
return await base.SendAsync(request, cancellationToken);
}
}
Есть ли способ обрабатывать этот вид аутентификации для приложения Blazor Server? Я просмотрел документацию здесь, и кажется, что Blazor Server на самом деле не имеет каких-либо надежных механизмов для аутентификации на стороне клиента или если запрос не выполняется на стороне клиента, то в более поздние моменты времени сеанса канала. У меня такое чувство, будто я пытаюсь навязать что-то, чего быть не должно, но у меня на данный момент нет множества вариантов изменить это из-за требований бизнеса.
Подробнее здесь: https://stackoverflow.com/questions/798 ... -with-grpc
Мобильная версия