Zwischenspeichern authentifizierter Anforderungen für alle Benutzer

9

Ich arbeite an einer Web-App, die sich mit sehr großen Impulsen gleichzeitiger Benutzer befassen muss, die autorisiert werden müssen, um identische Inhalte anzufordern. In seinem aktuellen Zustand ist es sogar für eine 32-Kern-AWS-Instanz völlig lähmend.

(Beachten Sie, dass wir Nginx als Reverse-Proxy verwenden.)

Die Antwort kann nicht einfach zwischengespeichert werden, da wir im schlimmsten Fall überprüfen müssen, ob der Benutzer authentifiziert ist, indem wir seine JWT dekodieren. Dies erfordert, dass wir Laravel 4 starten , was die meisten zustimmen würden, und dass es langsam ist , selbst wenn PHP-FPM und OpCache aktiviert sind. Dies ist hauptsächlich auf die heftige Bootstrapping-Phase zurückzuführen.

Man könnte die Frage stellen: "Warum haben Sie PHP und Laravel überhaupt verwendet, wenn Sie wussten, dass dies ein Problem sein würde?" - aber jetzt ist es zu spät, um auf diese Entscheidung zurückzukommen!

Mögliche Lösung

Eine vorgeschlagene Lösung besteht darin, das Auth-Modul aus Laravel in ein leichtes externes Modul (in etwas Schnellem wie C geschrieben) zu extrahieren, dessen Aufgabe es ist, das JWT zu dekodieren und zu entscheiden, ob der Benutzer authentifiziert ist.

Der Ablauf einer Anfrage wäre:

  1. Überprüfen Sie, ob der Cache getroffen wurde (falls nicht wie gewohnt an PHP übergeben)
  2. Dekodiere den Token
  3. Überprüfen Sie, ob es gültig ist
  4. Wenn gültig , aus dem Cache bereitstellen
  5. Wenn dies ungültig ist , teilen Sie dies Nginx mit. Anschließend leitet Nginx die Anforderung an PHP weiter, um sie wie gewohnt zu behandeln.

Dies ermöglicht es uns, PHP nicht zu drücken, sobald wir diese Anfrage an einen einzelnen Benutzer gesendet haben, und stattdessen ein leichtes Modul zu erreichen, um mit der Dekodierung von JWTs und anderen Vorbehalten, die mit dieser Art von Authentifizierung verbunden sind, herumzuspielen.

Ich dachte sogar daran, diesen Code direkt als Nginx-HTTP-Erweiterungsmodul zu schreiben.

Sorgen

Ich mache mir Sorgen, dass ich das noch nie gesehen habe und mich gefragt habe, ob es einen besseren Weg gibt.

Wenn Sie der Seite benutzerspezifischen Inhalt hinzufügen, wird diese Methode vollständig beendet.

Gibt es eine andere einfachere Lösung direkt in Nginx? Oder müssten wir etwas Spezialisierteres wie Lack verwenden?

Meine Fragen:

Ist die obige Lösung sinnvoll?

Wie wird das normalerweise angegangen?

Gibt es einen besseren Weg, um einen ähnlichen oder besseren Leistungsgewinn zu erzielen?

iamyojimbo
quelle
Ich habe mit einem ähnlichen Problem zu kämpfen. Einige Ideen a) Nginx auth_request kann möglicherweise an Ihren Authentifizierungs-Microservice übergeben werden, sodass Sie kein Nginx-Modul entwickeln müssen. b) Alternativ könnte Ihr Microservice authentifizierte Benutzer zu einer temporären URL umleiten, die öffentlich, zwischenspeicherbar und nicht erratbar ist, aber vom PHP-Backend auf einen begrenzten Zeitraum (den Cache-Zeitraum) überprüft werden kann. Dies beeinträchtigt die Sicherheit. Wenn die temporäre URL an einen nicht vertrauenswürdigen Benutzer weitergegeben wird, kann dieser für einen begrenzten Zeitraum auf den Inhalt zugreifen, ähnlich wie bei einem OAuth Bearer Token.
James
Haben Sie eine Lösung dafür gefunden? Ich
stehe
Es stellt sich heraus, dass wir durch einen großen Cluster optimierter Backend-Knoten in der Lage waren, die Last zu bewältigen - aber ich bin sehr zuversichtlich, dass dieser Ansatz langfristig eine große, kostensparende Lösung ist. Wenn Sie einige der Antworten kennen, die Sie möglicherweise im Voraus bereitstellen, und wenn Sie den Cache vor dem Zufluss von Anforderungen erwärmen, sind die Ressourceneinsparungen im Backend und der Zuverlässigkeitsgewinn sehr hoch.
iamyojimbo

Antworten:

9

Ich habe versucht, ein ähnliches Problem anzugehen. Meine Benutzer müssen für jede Anfrage authentifiziert werden. Ich habe mich darauf konzentriert, die Benutzer mindestens einmal von der Backend-App authentifizieren zu lassen (Validierung des JWT-Tokens), aber danach habe ich beschlossen, das Backend nicht mehr zu benötigen.

Ich habe mich dafür entschieden, kein Nginx-Plugin zu benötigen, das standardmäßig nicht enthalten ist. Andernfalls können Sie Nginx-JWT- oder Lua-Skripte überprüfen, und dies wären wahrscheinlich großartige Lösungen.

Adressierungsauthentifizierung

Bisher habe ich Folgendes getan:

  • Delegierte die Authentifizierung mit an Nginx auth_request. Dadurch wird ein internalSpeicherort aufgerufen, der die Anforderung an meinen Backend-Token-Validierungsendpunkt weiterleitet. Dies allein spricht das Problem der Behandlung einer hohen Anzahl von Validierungen überhaupt nicht an.

  • Das Ergebnis der Token-Validierung wird mithilfe einer proxy_cache_key "$cookie_token";Direktive zwischengespeichert. Nach erfolgreicher Token-Validierung fügt das Backend eine Cache-ControlAnweisung hinzu, die Nginx anweist, das Token nur bis zu 5 Minuten zwischenzuspeichern. Zu diesem Zeitpunkt befindet sich jedes einmal validierte Authentifizierungstoken im Cache. Nachfolgende Anforderungen desselben Benutzers / Tokens berühren das Authentifizierungs-Backend nicht mehr!

  • Um meine Backend-App vor einer möglichen Überflutung durch ungültige Token zu schützen, habe ich auch abgelehnte Validierungen zwischengespeichert, wenn mein Backend-Endpunkt 401 zurückgibt. Diese werden nur für kurze Zeit zwischengespeichert, um zu vermeiden, dass der Nginx-Cache möglicherweise mit solchen Anforderungen gefüllt wird.

Ich habe einige zusätzliche Verbesserungen hinzugefügt, z. B. einen Abmeldeendpunkt, der ein Token ungültig macht, indem 401 zurückgegeben wird (das auch von Nginx zwischengespeichert wird), sodass das Token nicht mehr verwendet werden kann, wenn der Benutzer auf Abmelden klickt, auch wenn es nicht abgelaufen ist.

Außerdem enthält mein Nginx-Cache für jedes Token den zugeordneten Benutzer als JSON-Objekt, sodass ich ihn nicht aus der Datenbank abrufen kann, wenn ich diese Informationen benötige. und erspart mir auch das Entschlüsseln des Tokens.

Informationen zur Token-Lebensdauer und zum Aktualisieren von Token

Nach 5 Minuten ist das Token im Cache abgelaufen, sodass das Backend erneut abgefragt wird. Dies soll sicherstellen, dass Sie ein Token ungültig machen können, weil sich der Benutzer abmeldet, weil es kompromittiert wurde und so weiter. Durch eine solche regelmäßige erneute Validierung mit ordnungsgemäßer Implementierung im Backend muss ich keine Aktualisierungstoken verwenden.

Herkömmlicherweise werden Aktualisierungstoken verwendet, um ein neues Zugriffstoken anzufordern. Sie werden in Ihrem Backend gespeichert und Sie überprüfen, ob eine Anforderung für ein Zugriffstoken mit einem Aktualisierungstoken erfolgt, das mit dem in der Datenbank für diesen bestimmten Benutzer übereinstimmt. Wenn sich der Benutzer abmeldet oder Token gefährdet sind, löschen / ungültig machen Sie das Aktualisierungstoken in Ihrer Datenbank, sodass die nächste Anforderung eines neuen Tokens mit dem ungültig gemachten Aktualisierungstoken fehlschlägt.

Kurz gesagt, Aktualisierungstoken haben normalerweise eine lange Gültigkeit und werden immer mit dem Backend verglichen. Sie werden verwendet, um Zugriffstoken mit einer sehr kurzen Gültigkeit (einige Minuten) zu generieren. Diese Zugriffstoken erreichen normalerweise Ihr Backend, aber Sie überprüfen nur deren Signatur und Ablaufdatum.

Hier in meinem Setup verwenden wir Token mit einer längeren Gültigkeit (kann Stunden oder ein Tag sein), die dieselbe Rolle und dieselben Funktionen haben wie ein Zugriffstoken und ein Aktualisierungstoken. Da die Validierung und Invalidierung von Nginx zwischengespeichert wird, werden sie vom Backend nur einmal alle 5 Minuten vollständig überprüft. So behalten wir den Vorteil der Verwendung von Aktualisierungstoken (um ein Token schnell ungültig zu machen) ohne die zusätzliche Komplexität. Und eine einfache Validierung erreicht niemals Ihr Backend, das mindestens eine Größenordnung langsamer ist als der Nginx-Cache, selbst wenn es nur zur Überprüfung der Signatur und des Ablaufdatums verwendet wird.

Mit diesem Setup konnte ich die Authentifizierung in meinem Backend deaktivieren, da alle eingehenden Anforderungen die auth_requestNginx-Direktive erreichen, bevor sie berührt werden.

Damit ist das Problem nicht vollständig gelöst, wenn Sie eine Autorisierung pro Ressource durchführen müssen, aber zumindest den grundlegenden Autorisierungsteil gespeichert haben. Sie können sogar vermeiden, das Token zu entschlüsseln oder eine DB-Suche durchzuführen, um auf Token-Daten zuzugreifen, da die zwischengespeicherte Nginx-Authentifizierungsantwort Daten enthalten und an das Backend zurückgeben kann.

Jetzt ist meine größte Sorge, dass ich etwas Offensichtliches in Bezug auf Sicherheit brechen kann, ohne es zu merken. Davon abgesehen wird jedes empfangene Token noch mindestens einmal validiert, bevor es von Nginx zwischengespeichert wird. Jedes temperierte Token wäre anders, würde also nicht in den Cache gelangen, da auch der Cache-Schlüssel anders wäre.

Vielleicht ist es auch erwähnenswert, dass eine Authentifizierung in der realen Welt gegen das Stehlen von Token kämpfen würde, indem eine zusätzliche Nonce oder etwas anderes generiert (und überprüft) wird.

Hier ist ein vereinfachter Auszug meiner Nginx-Konfiguration für meine App:

# Cache for internal auth checks
proxy_cache_path /usr/local/var/nginx/cache/auth levels=1:2 keys_zone=auth_cache:10m max_size=128m inactive=10m use_temp_path=off;
# Cache for content
proxy_cache_path /usr/local/var/nginx/cache/resx levels=1:2 keys_zone=content_cache:16m max_size=128m inactive=5m use_temp_path=off;
server {
    listen 443 ssl http2;
    server_name ........;

    include /usr/local/etc/nginx/include-auth-internal.conf;

    location /api/v1 {
        # Auth magic happens here
        auth_request         /auth;
        auth_request_set     $user $upstream_http_X_User_Id;
        auth_request_set     $customer $upstream_http_X_Customer_Id;
        auth_request_set     $permissions $upstream_http_X_Permissions;

        # The backend app, once Nginx has performed internal auth.
        proxy_pass           http://127.0.0.1:5000;
        proxy_set_header     X-User-Id $user;
        proxy_set_header     X-Customer-Id $customer;
        proxy_set_header     X-Permissions $permissions;

        # Cache content
        proxy_cache          content_cache;
        proxy_cache_key      "$request_method-$request_uri";
    }
    location /api/v1/Logout {
        auth_request         /auth/logout;
    }

}

Hier ist der Konfigurationsextrakt für den internen /authEndpunkt, der oben aufgeführt ist als /usr/local/etc/nginx/include-auth-internal.conf:

# Called before every request to backend
location = /auth {
    internal;
    proxy_cache             auth_cache;
    proxy_cache_methods     GET HEAD POST;
    proxy_cache_key         "$cookie_token";
    # Valid tokens cache duration is set by backend returning a properly set Cache-Control header
    # Invalid tokens are shortly cached to protect backend but not flood Nginx cache
    proxy_cache_valid       401 30s;
    # Valid tokens are cached for 5 minutes so we can get the backend to re-validate them from time to time
    proxy_cache_valid       200 5m;
    proxy_pass              http://127.0.0.1:1234/auth/_Internal;
    proxy_set_header        Host ........;
    proxy_pass_request_body off;
    proxy_set_header        Content-Length "";
    proxy_set_header        Accept application/json;
}

# To invalidate a not expired token, use a specific backend endpoint.
# Then we cache the token invalid/401 response itself.
location = /auth/logout {
    internal;
    proxy_cache             auth_cache;
    proxy_cache_key         "$cookie_token";
    # Proper caching duration (> token expire date) set by backend, which will override below default duration
    proxy_cache_valid       401 30m;
    # A Logout requests forces a cache refresh in order to store a 401 where there was previously a valid authorization
    proxy_cache_bypass      1;

    # This backend endpoint always returns 401, with a cache header set to the expire date of the token
    proxy_pass              http://127.0.0.1:1234/auth/_Internal/Logout;
    proxy_set_header        Host ........;
    proxy_pass_request_body off;
}

.

Adressierung von Inhalten

Jetzt wird die Authentifizierung von den Daten getrennt. Da Sie angegeben haben, dass es für jeden Benutzer identisch ist, kann der Inhalt selbst auch von Nginx zwischengespeichert werden (in meinem Beispiel in der content_cacheZone).

Skalierbarkeit

Dieses Szenario funktioniert sofort, vorausgesetzt, Sie haben einen Nginx-Server. In einem realen Szenario haben Sie wahrscheinlich eine hohe Verfügbarkeit, dh mehrere Nginx-Instanzen, die möglicherweise auch Ihre (Laravel-) Backend-Anwendung hosten. In diesem Fall kann jede Anfrage Ihrer Benutzer an einen Ihrer Nginx-Server gesendet werden. Bis alle das Token lokal zwischengespeichert haben, erreichen sie Ihr Backend, um es zu überprüfen. Für eine kleine Anzahl von Servern würde die Verwendung dieser Lösung immer noch große Vorteile bringen.

Es ist jedoch wichtig zu beachten, dass Sie bei mehreren Nginx-Servern (und damit Caches) die Möglichkeit verlieren, sich auf der Serverseite abzumelden, da Sie den Token-Cache nicht auf allen löschen können (indem Sie eine Aktualisierung erzwingen) /auth/logouttut in meinem Beispiel. Sie haben nur noch die 5-Minuten-Token-Cache-Dauer, die dazu führt, dass Ihr Backend bald abgefragt wird, und Nginx mitteilt, dass die Anforderung abgelehnt wird. Eine teilweise Problemumgehung besteht darin, den Token-Header oder das Cookie auf dem Client beim Abmelden zu löschen.

Jeder Kommentar wäre sehr willkommen und dankbar!

mbarthelemy
quelle
Sie sollten viel mehr Upvotes bekommen! Sehr hilfreich, danke!
Gershon Papi
"Ich habe einige zusätzliche Verbesserungen hinzugefügt, z. B. einen Abmeldeendpunkt, der ein Token ungültig macht, indem 401 zurückgegeben wird (das auch von Nginx zwischengespeichert wird), sodass das Token nicht mehr verwendet werden kann, wenn der Benutzer auf Abmelden klickt, auch wenn es nicht abgelaufen ist. "" - Das ist klug! , aber setzen Sie das Token tatsächlich auch in Ihrem Backend auf die schwarze Liste, sodass der Benutzer sich immer noch nicht mit diesem bestimmten Token anmelden kann, falls der Cache ausfällt oder so?
Gaurav5430
"Es ist jedoch wichtig zu beachten, dass Sie bei mehreren Nginx-Servern (und damit Caches) die Möglichkeit verlieren, sich auf der Serverseite abzumelden, da Sie den Token-Cache nicht auf allen löschen können (indem Sie eine Aktualisierung erzwingen). wie / auth / logout in meinem Beispiel. " können Sie näher darauf eingehen?
Gaurav5430