使用JWT在.NET Core gRpc中进行身份验证

在本文中,我将讨论使用JWT的gRpc服务中API身份验证的功能。我假设您熟悉在.NET Core WebAPI中使用它们的JWT和HTTP标头,因此我将不讨论这些详细信息。当我尝试在gRpc中实现身份验证时,我发现一个事实,即大多数示例都是使用控制台应用程序编写的。我认为,这与开发人员所生活的现实相距太远。例如,我不想每次调用服务方法时都创建一个通道。我也不想担心随每个请求发送令牌和用户信息。相反,我希望拥有一个基础架构级别,它将为我解决所有这些问题。如果您对这个主题感兴趣,那么将会有更多的内容可供选择。本文中的所有示例均对.NET Core 3.1有效。

二手例子


在深入探讨该主题之前,值得描述一下本文中使用的示例。整个解决方案由两个应用程序组成:网站和gRpc服务(以下称为API)。两者都是用.NET Core 3.1编写的。如果用户有权登录,则可以登录并查看一些数据。该网站不存储用户数据,并且在身份验证过程中依赖于API。为了与gRpc服务进行通信,网站需要具有有效的JWT令牌,但是该令牌与应用程序中的用户身份验证无关。 Web应用程序在其一侧使用cookie。为了使API知道哪个用户向服务发出了请求,有关此信息的信息与JWT令牌一起发送,但不是在令牌本身中发送,而是与附加的HTTP标头一起发送。下图显示了我刚才讨论的系统的示例架构:


在这里,我应该注意,当我做这个例子时,我的目标不是为API实现最正确的身份验证方法。如果您想了解一些最佳实践,请查看OpenID Connect规范虽然,有时在我看来,最正确的解决方案与可以解决问题并节省时间和金钱的解决方案相比可能是多余的。

在gRpc服务中启用JWT身份验证


gRpc服务的配置与.NET Core API所需的常规配置没有什么不同。另一个优点是HTTP和HTTPS协议没有什么不同。简要地说,您需要在Startup.cs文件中添加标准的身份验证和授权服务以及Middlewe添加中间件的位置很重要:您需要在路由和点之间精确添加中间件(缺少一些代码):

public void Configure(...) {
    app.UseRouting();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.UseEndpoints(...
}

但是注册服务的地方并不那么重要,只需将ConfigureServices()方法添加到方法中但是在这里,您需要配置JWT令牌检查。可以在这里定义它,但是我建议将其放入单独的类中。因此,代码可能如下所示:

public void ConfigureServices(...) {
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(o => {
            var validator = new JwtTokenValidator(...);
            o.SecurityTokenValidators.Add(validator);
        });
    services.AddAuthorization();
}

JwtTokenValidator是定义验证逻辑的类。您需要使用正确的设置来创建TokenValidationParameters,它将完成其余的JWT验证工作。作为奖励,您可以在此处添加额外的安全层。因为JWT是一种众所周知的格式,所以可能需要它。如果您拥有JWT,则可以转到jwt.io并查看一些信息。我更喜欢为JWT添加额外的加密,这使解密更加困难。验证器的外观如下:

public class JwtTokenValidator : ISecurityTokenValidator
{
    public bool CanReadToken(string securityToken) => true;
    
    public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
    {
        var handler = new JwtSecurityTokenHandler();
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "your string",
            ValidAudience = "your string",
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your secrete code"))
        };
        
        var claimsPrincipal = handler.ValidateToken(token, tokenValidationParameters, out validatedToken);
        return claimsPrincipal;
    }
    
    public bool CanValidateToken { get; } = true;
    public int MaximumTokenSizeInBytes { get; set; } = int.MaxValue;
}

这就是API方面的全部需求。根据所选的HTTP或HTTPS协议,客户端设置历史记录会稍长一些,并且会略有不同。

将每个请求的HTTP标头发送到gRpc服务


您可能从官方文档中知道了这一点,实际上,您只能在哑控制台程序中使用它。例如,您可以在此处看到

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(
    new HelloRequest { Name = "World" });
Console.WriteLine(response.Message);

要在实际项目中使用它,我们仍然需要具有集中式的配置和DI,而这几乎是没有考虑的。这是您需要做的。首先,我们需要在项目中添加必要的NuGet包。Grpc.Tools将帮助您在构建项目时创建原型,而Grpc.Net.ClientFactory将帮助您设置DI。 使用gRpc时,如果需要在请求-响应链中间的某个地方实现处理,则需要使用从Interceptor继承的类该类gRpc.Core的一部分。如果需要在服务内部访问HttpContext.User.Identity,则可以添加IHttpContextAccessor接口

dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
dotnet add package Grpc.Net.ClientFactory



您的服务(这需要在服务中进行其他注册)。您需要将以下内容添加到Startup.cs文件中。

services.AddTransient<AuthHeadersInterceptor>();
services.AddHttpContextAccessor();

var httpClientBuilder = services.AddGrpcClient<MygRpcService.MygRpcServiceClient>(o => { o.Address = new Uri("grpc-endpoint-url"); });
httpClientBuilder.AddInterceptor<AuthHeadersInterceptor>();              
httpClientBuilder.ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);

AuthHeadersInterceptor是我们自己的类,它是从Interceptor类派生的它使用IHttpContextAccessor并注册.AddHttpContextAccessor()允许您执行此操作。

HTTP的配置功能


您可能会注意到以下配置:

httpClientBuilder.ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);

必须通过HTTP进行工作,但这还不够。您还需要从Configure()方法中排除此行。

app.UseHttpsRedirection();

而且,在创建任何gRpc通道之前,您仍然需要跳舞以建立特殊的设置。在应用程序启动期间,此操作只能执行一次。因此,我将其添加到与上述删除行几乎相同的位置。仅应为HTTP调用。

AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

HTTPS的配置功能


在Windows和Linux上使用SSL会遇到一些困难。您可能会在Windows计算机上进行开发,并使用基于Linux的映像将其部署到Docker / Kubernetes。在这种情况下,配置不像许多文章中所描述的那样简单。我将在另一篇文章中描述此配置,在这里我仅涉及代码。

我们需要重新配置gRpc通道以使用SSL凭据。如果部署到Docker并制作基于Linux的映像,则可能还需要将HttpClient配置为允许无效证书。为每个通道创建HttpClient。

httpClientBuilder.ConfigureChannel(o =>
{
    // add SSL credentials
    o.Credentials = new SslCredentials();
    // allow invalid/untrusted certificates
    var httpClientHandler = new HttpClientHandler
    {
        ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    };
    var httpClient = new HttpClient(httpClientHandler);
    o.HttpClient = httpClient;
});

添加HTTP头


标头被添加到拦截器类(Interceptor的后继者)中。gRpc使用元数据的概念,它与请求一起作为标头发送。拦截器类应为调用上下文添加元数据。

public class AuthHeadersInterceptor : Interceptor
{
    public AuthHeadersInterceptor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var metadata = new Metadata
        {
            {HttpHeaderNames.Authorization, $"Bearer <JWT_TOKEN>"}
        };
        var userIdentity = _httpContextAccessor.HttpContext.User.Identity;
        if (userIdentity.IsAuthenticated)
        {
            metadata.Add(HttpHeaderNames.User, userIdentity.Name);
        }
        var callOption = context.Options.WithHeaders(metadata);
        context = new ClientInterceptorContext<TRequest, TResponse>(context.Method, context.Host, callOption);
        
        return base.AsyncUnaryCall(request, context, continuation);
    }
}

对于这种情况,当您仅调用gRpc服务时,只需要覆盖AsyncUnaryCall方法当然,JWT令牌可以保存在配置文件中。

这就是全部。稍后,我将使用描述的用例的简单示例添加到代码的链接。如果您还有其他问题,请写信给我。我会尽力回答。

All Articles