我根据 Microsoft 在 .NET Core 3.2 中的 Hosted Blazor WebAssembly 项目的新项目模板解决了这个问题。我从 BaseAddressAuthorizationMessageHandler 复制了代码,但注释掉了令牌不可用时引发的异常,并将其添加到 Program.cs 中的 HttpClient:
Program.cs:
builder.Services.AddHttpClient("SampleProject.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<GrpcWebHandler>()
.AddHttpMessageHandler<GrpcAuthorizationMessageHandler>();
builder.Services.AddSingleton(services =>
{
// Create a gRPC-Web channel pointing to the backend server
var httpClient = services.GetRequiredService<HttpClient>();
var baseUri = services.GetRequiredService<NavigationManager>().BaseUri;
var channel = GrpcChannel.ForAddress(baseUri, new GrpcChannelOptions { HttpClient = httpClient });
// Now we can instantiate gRPC clients for this channel
return new Products.ProductsClient(channel);
});
GrpcAuthorizationMessageHandler.cs (source):
public class GrpcAuthorizationMessageHandler : DelegatingHandler
{
private readonly IAccessTokenProvider _provider;
private readonly NavigationManager _navigation;
private AccessToken _lastToken;
private AuthenticationHeaderValue _cachedHeader;
private Uri[] _authorizedUris;
private AccessTokenRequestOptions _tokenOptions;
public GrpcAuthorizationMessageHandler(
IAccessTokenProvider provider,
NavigationManager navigation)
{
_provider = provider;
_navigation = navigation;
ConfigureHandler(new[] { _navigation.BaseUri });
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var now = DateTimeOffset.Now;
if (_authorizedUris == null)
{
throw new InvalidOperationException($"The '{nameof(AuthorizationMessageHandler)}' is not configured. " +
$"Call '{nameof(AuthorizationMessageHandler.ConfigureHandler)}' and provide a list of endpoint urls to attach the token to.");
}
if (_authorizedUris.Any(uri => uri.IsBaseOf(request.RequestUri)))
{
if (_lastToken == null || now >= _lastToken.Expires.AddMinutes(-5))
{
var tokenResult = _tokenOptions != null ?
await _provider.RequestAccessToken(_tokenOptions) :
await _provider.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
_lastToken = token;
_cachedHeader = new AuthenticationHeaderValue("Bearer", _lastToken.Value);
}
// this exception was commented out to be used with the GrpcWebHandler
// else
// {
// throw new AccessTokenNotAvailableException(_navigation, tokenResult, _tokenOptions?.Scopes);
// }
}
// We don't try to handle 401s and retry the request with a new token automatically since that would mean we need to copy the request
// headers and buffer the body and we expect that the user instead handles the 401s. (Also, we can't really handle all 401s as we might
// not be able to provision a token without user interaction).
request.Headers.Authorization = _cachedHeader;
}
return await base.SendAsync(request, cancellationToken);
}
public GrpcAuthorizationMessageHandler ConfigureHandler(
IEnumerable<string> authorizedUrls,
IEnumerable<string> scopes = null,
string returnUrl = null)
{
if (_authorizedUris != null)
{
throw new InvalidOperationException("Handler already configured.");
}
if (authorizedUrls == null)
{
throw new ArgumentNullException(nameof(authorizedUrls));
}
var uris = authorizedUrls.Select(uri => new Uri(uri, UriKind.Absolute)).ToArray();
if (uris.Length == 0)
{
throw new ArgumentException("At least one URL must be configured.", nameof(authorizedUrls));
}
_authorizedUris = uris;
var scopesList = scopes?.ToArray();
if (scopesList != null || returnUrl != null)
{
_tokenOptions = new AccessTokenRequestOptions
{
Scopes = scopesList,
ReturnUrl = returnUrl
};
}
return this;
}
}
这是其背后的基本原理。
根据 Steve Sanderson 的this blog post,您只需将 GrpcWebHandler 添加到 HttpClient 即可使用 GrpcWeb。但是,如果您尝试将 BaseAddressAuthorizationMessageHandler 与 GrpcWebHandler 一起使用,您将在用户未经身份验证时收到带有 StatusCode=Internal 的 RpcException。
查看代码后发现异常的原因是授权处理程序在token不可用时抛出异常,而GrpcWebHandler将其作为内部异常捕获。如果您添加一个不会抛出该异常的自定义消息处理程序(如上面的那个),GrpcWebHandler 将抛出 StatusCode=Unauthenticated 的正确 RcpException,然后您可以相应地处理它,例如通过重定向到登录页面。
这是一个示例,说明如何在 razor 页面中使用 GrpcClient 而无需添加额外的授权代码:
@inject CustomClient grpcClient
@inject NavigationManager navManager
@code {
public async Task MakeRequest() {
var request = new Request();
try
{
var reply = await grpcClient.MakeRequestAsync(request);
}
catch (Grpc.Core.RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated)
{
NavigationManager.NavigateTo($"/authentication/login/?returnUrl={NavigationManager.BaseUri}your-page");
}
}
}