Authentifizierung in .NET Core gRpc mit JWT

In diesem Artikel werde ich über die Funktionen der API-Authentifizierung in gRpc-Diensten mit JWT sprechen. Ich gehe davon aus, dass Sie mit JWT- und HTTP-Headern vertraut sind, die sie in .NET Core WebAPI verwenden, daher werde ich diese Details nicht diskutieren. Als ich versuchte, die Authentifizierung in gRpc zu implementieren, stieß ich auf die Tatsache, dass die meisten Beispiele mit Konsolenanwendungen geschrieben wurden. Dies ist zu weit von der Realität entfernt, in der Entwickler meiner Meinung nach leben. Beispielsweise möchte ich nicht jedes Mal einen Kanal erstellen, wenn ich eine Dienstmethode aufrufen möchte. Ich möchte mich auch nicht darum kümmern, bei jeder Anfrage ein Token und Benutzerinformationen zu senden. Stattdessen möchte ich eine Infrastrukturebene haben, die all dies für mich erledigt. Wenn dieses Thema für Sie interessant ist, wird es mehr unter dem Schnitt geben. Alle Beispiele in diesem Artikel gelten für .NET Core 3.1.

Verwendetes Beispiel


Bevor Sie sich mit dem Thema befassen, sollten Sie das im Artikel verwendete Beispiel beschreiben. Die gesamte Lösung besteht aus zwei Anwendungen: einer Website und einem gRpc-Dienst (im Folgenden: API). Beide sind in .NET Core 3.1 geschrieben. Der Benutzer kann sich anmelden und einige Daten anzeigen, wenn er dazu berechtigt ist. Die Website speichert keine Benutzerdaten und stützt sich bei der Authentifizierung auf eine API. Für die Kommunikation mit dem gRpc-Dienst muss die Website über ein gültiges JWT-Token verfügen. Dieses Token gilt jedoch nicht für die Benutzerauthentifizierung in der Anwendung. Die Webanwendung verwendet Cookies auf ihrer Seite. Damit die API weiß, welcher Benutzer die Anforderung an den Dienst stellt, werden Informationen dazu zusammen mit dem JWT-Token gesendet, jedoch nicht im Token selbst, sondern mit einem zusätzlichen HTTP-Header. Die folgende Abbildung zeigt ein Beispielschema des Systems, über das ich gerade gesprochen habe:


Hier sollte ich beachten, dass ich bei diesem Beispiel nicht das Ziel hatte, die korrekteste Authentifizierungsmethode für die API zu implementieren. Wenn Sie einige bewährte Methoden anzeigen möchten, lesen Sie die OpenID Connect-Spezifikation . Manchmal scheint es mir jedoch, dass die korrekteste Lösung im Vergleich zu dem, was das Problem lösen und Zeit und Geld sparen kann, überflüssig sein kann.

Aktivieren Sie die JWT-Authentifizierung im gRpc-Dienst


Die Konfiguration des gRpc-Dienstes unterscheidet sich nicht von der üblichen Konfiguration, die für die .NET Core-API erforderlich ist. Ein zusätzliches Plus ist, dass es für HTTP- und HTTPS-Protokolle nicht anders ist. Kurz gesagt, Sie müssen Standardauthentifizierungs- und Autorisierungsdienste sowie Middlewere in der Datei Startup.cs hinzufügen . Der Ort, an dem Sie Middleware hinzufügen, ist wichtig: Sie müssen sie genau zwischen Routing und Punkten hinzufügen (ein Teil des Codes fehlt ):

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

Der Ort, an dem die Dienste registriert sind, ist jedoch nicht so wichtig. Fügen Sie der Methode einfach die ConfigureServices () -Methode hinzu . Hier müssen Sie jedoch die JWT-Token-Prüfung konfigurieren. Es kann genau hier definiert werden, aber ich empfehle, es in eine separate Klasse zu ziehen. Daher kann der Code folgendermaßen aussehen:

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

In der JwtTokenValidator- Klasse definieren Sie die Validierungslogik. Sie müssen die TokenValidationParameters- Klasse mit den richtigen Einstellungen erstellen und sie erledigt den Rest der JWT-Validierungsarbeit. Als Bonus können Sie hier eine zusätzliche Sicherheitsebene hinzufügen. Dies kann erforderlich sein, da JWT ein bekanntes Format ist. Wenn Sie JWT haben, können Sie auf jwt.io einige Informationen anzeigen . Ich bevorzuge es, dem JWT eine zusätzliche Verschlüsselung hinzuzufügen, was die Entschlüsselung schwieriger macht. So könnte ein Validator aussehen:

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;
}

Und das ist alles, was die API-Seite benötigt. Der Client-Setup-Verlauf ist je nach ausgewähltem HTTP- oder HTTPS-Protokoll etwas länger und unterschiedlich.

Senden von HTTP-Headern bei jeder Anforderung an den gRpc-Dienst


Möglicherweise kennen Sie diese aus der offiziellen Dokumentation, die Sie nur in einem dummen Konsolenprogramm verwenden können. Zum Beispiel können Sie es hier sehen .

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);

Um dies in einem realen Projekt zu verwenden, benötigen wir noch eine zentralisierte Konfiguration und DI, die fast nicht berücksichtigt werden. Hier ist was Sie tun müssen. Zunächst müssen wir unserem Projekt die erforderlichen NuGet-Pakete hinzufügen. Mit dem Grpc.Tools- Paket können Sie beim Erstellen eines Projekts Prototypen erstellen, und mit Grpc.Net.ClientFactory können Sie DI einrichten. Wenn Sie bei der Arbeit mit gRpc Ihre Verarbeitung irgendwo in der Mitte der Anforderungs- / Antwortkette implementieren müssen, müssen Sie Klassen verwenden, die von Interceptor geerbt wurden , das Teil von gRpc.Core ist . Wenn Sie in Ihren Diensten auf HttpContext.User.Identity zugreifen müssen , können Sie die IHttpContextAccessor- Schnittstelle hinzufügen

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



zu Ihrem Dienst (dies erfordert eine zusätzliche Registrierung in den Diensten). Sie müssen Ihrer Datei Startup.cs Folgendes hinzufügen.

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);

Die AuthHeadersInterceptor- Klasse ist unsere eigene Klasse, die von der Interceptor- Klasse abgeleitet ist . Es verwendet IHttpContextAccessor und die Registrierung von .AddHttpContextAccessor () ermöglicht Ihnen dies.

Konfigurationsfunktionen für HTTP


Möglicherweise stellen Sie die folgende Konfiguration fest:

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

Es ist notwendig, um über HTTP zu arbeiten, aber das reicht nicht aus. Sie müssen diese Zeile auch von der Configure () -Methode ausschließen.

app.UseHttpsRedirection();

Und dennoch müssen Sie tanzen, um eine spezielle Einstellung festzulegen, bevor Sie einen gRpc-Kanal erstellen. Dies kann während des Anwendungsstarts nur einmal durchgeführt werden. Daher habe ich es fast an der gleichen Stelle wie die oben erwähnte gelöschte Zeile hinzugefügt. Dies sollte nur für HTTP aufgerufen werden.

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

Konfigurationsfunktionen für HTTPS


Es gibt einige Schwierigkeiten bei der Arbeit mit SSL unter Windows und Linux. Es kann vorkommen, dass Sie auf einem Windows-Computer entwickeln und mithilfe von Linux-basierten Images auf Docker / Kubernetes bereitstellen. In diesem Fall ist die Konfiguration nicht so einfach wie in vielen Beiträgen beschrieben. Ich werde diese Konfiguration in einem anderen Artikel beschreiben und hier nur auf den Code eingehen.

Wir müssen den gRpc-Kanal neu konfigurieren, um SSL-Anmeldeinformationen zu verwenden. Wenn Sie in Docker bereitstellen und Linux-basierte Images erstellen, müssen Sie möglicherweise auch HttpClient konfigurieren, um ungültige Zertifikate zuzulassen. HttpClient wird für jeden Kanal erstellt.

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;
});

Hinzufügen von HTTP-Headern


Header werden in der Interceptor-Klasse hinzugefügt (Nachfolger von Interceptor ). gRpc verwendet das Konzept von Metadaten, die zusammen mit Anforderungen als Header gesendet werden. Die Interceptor-Klasse sollte Metadaten für den Aufrufkontext hinzufügen.

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);
    }
}

Wenn Sie in diesem Szenario nur den gRpc-Dienst aufrufen, müssen Sie nur die AsyncUnaryCall- Methode überschreiben . Natürlich kann das JWT-Token in Konfigurationsdateien gespeichert werden.

Und das ist alles. Später werde ich einen Link zum Code mit einem einfachen Beispiel für den beschriebenen Anwendungsfall hinzufügen. Wenn Sie weitere Fragen haben, schreiben Sie mir bitte. Ich werde versuchen zu antworten.

All Articles