Mise en cache des demandes authentifiées pour tous les utilisateurs


9

Je travaille sur une application web qui doit gérer de très fortes impulsions d'utilisateurs simultanés, qui doivent être autorisés, à demander un contenu identique. Dans son état actuel, il est totalement paralysant, même pour une instance AWS à 32 cœurs.

(Notez que nous utilisons Nginx comme proxy inverse)

La réponse ne peut pas être simplement mise en cache car, dans le pire des cas, il faut vérifier si l'utilisateur est authentifié en décodant son JWT. Cela nous oblige à lancer Laravel 4, ce que la plupart conviendront, est lent , même avec PHP-FPM et OpCache activés. Cela est principalement dû à la phase de bootstrapping lourde.

On pourrait se poser la question "Pourquoi avez-vous utilisé PHP et Laravel en premier lieu si vous saviez que cela allait être un problème?" - mais il est trop tard maintenant pour revenir sur cette décision!

Solution possible

Une solution qui a été proposée est d'extraire le module Auth de Laravel vers un module externe léger (écrit en quelque chose de rapide comme C) dont la responsabilité est de décoder le JWT et de décider si l'utilisateur est authentifié.

Le flux d'une demande serait:

  1. Vérifiez si le cache est atteint (sinon passez à PHP comme d'habitude)
  2. Décoder le jeton
  3. Vérifiez s'il est valide
  4. Si valide , servir à partir du cache
  5. S'il n'est pas valide , dites-le à Nginx, puis Nginx transmettra ensuite la demande à PHP pour traiter normalement.

Cela nous permettra de ne pas frapper PHP une fois que nous aurons servi cette demande à un seul utilisateur et à la place de contacter un module léger pour déconner avec les JWT de décodage et toutes les autres mises en garde fournies avec ce type d'authentification.

Je pensais même à écrire ce code directement en tant que module d'extension HTTP Nginx.

Préoccupations

Ma préoccupation est que je n'ai jamais vu cela se faire auparavant et je me suis demandé s'il y avait une meilleure façon.

De plus, la seconde où vous ajoutez un contenu spécifique à la page, cela tue totalement cette méthode.

Existe-t-il une autre solution plus simple disponible directement dans Nginx? Ou devrions-nous utiliser quelque chose de plus spécialisé comme le vernis?

Mes questions:

La solution ci-dessus est-elle logique?

Comment cela est-il normalement abordé?

Existe-t-il un meilleur moyen d'obtenir un gain de performances similaire ou meilleur?


Je suis aux prises avec un problème similaire. Quelques idées a) Nginx auth_request peut être en mesure de transmettre à votre microservice d'authentification, allégeant ainsi le besoin de développer un module Nginx. b) Alternativement, votre microservice pourrait rediriger les utilisateurs authentifiés vers une URL temporaire qui est publique, pouvant être mise en cache et impossible à deviner, mais qui peut être validée par le backend PHP pour être valide pour une période limitée (la période de cache). Cela sacrifie une certaine sécurité, si l'URL temporaire est divulguée à un utilisateur non fiable, il peut accéder au contenu pendant cette période limitée, un peu comme un jeton de porteur OAuth.
James

Avez-vous trouvé une solution à cela? Je fais face à la même chose
timbroder

Il s'avère qu'en ayant un grand cluster de nœuds back-end optimisés, nous avons été en mesure de faire face à la charge - mais j'ai une grande confiance dans cette approche étant une grande solution d'économie à long terme. Si vous connaissez certaines des réponses que vous pourriez servir à l'avance, si vous réchauffez le cache avant l'afflux de demandes, l'économie de ressources et le gain de fiabilité du backend seraient très élevés.
iamyojimbo

Réponses:


9

J'ai essayé de résoudre un problème similaire. Mes utilisateurs doivent être authentifiés pour chaque demande qu'ils font. Je me suis concentré sur l'authentification des utilisateurs au moins une fois par l'application backend (validation du token JWT), mais après cela, j'ai décidé de ne plus avoir besoin du backend.

J'ai choisi d'éviter d'exiger tout plugin Nginx qui n'est pas inclus par défaut. Sinon, vous pouvez vérifier les scripts nginx-jwt ou Lua et ce seraient probablement d'excellentes solutions.

Adressage de l'authentification

Jusqu'à présent, j'ai fait ce qui suit:

  • Délégué l'authentification à Nginx à l'aide de auth_request. Cela appelle un internalemplacement qui transmet la demande à mon point de terminaison de validation de jeton d'arrière-plan. Cela ne résout pas à lui seul la question du traitement d'un nombre élevé de validations.

  • Le résultat de la validation du jeton est mis en cache à l'aide d'une proxy_cache_key "$cookie_token";directive. Une fois la validation du jeton réussie, le backend ajoute une Cache-Controldirective qui indique à Nginx de ne mettre en cache le jeton que pendant 5 minutes maximum. À ce stade, tout jeton d'authentification validé une fois se trouve dans le cache, les demandes ultérieures du même utilisateur / jeton ne touchent plus le backend d'authentification!

  • Pour protéger mon application backend contre les inondations potentielles par des jetons invalides, je cache également les validations refusées, lorsque mon point de terminaison backend renvoie 401. Celles-ci ne sont mises en cache que pendant une courte durée pour éviter de potentiellement remplir le cache Nginx avec de telles demandes.

J'ai ajouté quelques améliorations supplémentaires telles qu'un point de terminaison de déconnexion qui invalide un jeton en renvoyant 401 (qui est également mis en cache par Nginx) afin que si l'utilisateur clique sur déconnexion, le jeton ne puisse plus être utilisé même s'il n'est pas expiré.

De plus, mon cache Nginx contient pour chaque jeton, l'utilisateur associé en tant qu'objet JSON, ce qui m'évite de le récupérer dans la base de données si j'ai besoin de ces informations; et me sauve également du décryptage du jeton.

À propos de la durée de vie des jetons et des jetons d'actualisation

Après 5 minutes, le jeton aura expiré dans le cache, donc le backend sera à nouveau interrogé. Il s'agit de garantir que vous pouvez invalider un jeton, car l'utilisateur se déconnecte, car il a été compromis, etc. Une telle revalidation périodique, avec une implémentation appropriée dans le backend, m'évite d'avoir à utiliser des jetons de rafraîchissement.

Traditionnellement, les jetons d'actualisation sont utilisés pour demander un nouveau jeton d'accès; ils seraient stockés dans votre backend et vous vérifieriez qu'une demande de jeton d'accès est faite avec un jeton d'actualisation qui correspond à celui que vous avez dans la base de données pour cet utilisateur spécifique. Si l'utilisateur se déconnecte ou si les jetons sont compromis, vous supprimez / invalidez le jeton d'actualisation dans votre base de données afin que la prochaine demande de nouveau jeton utilisant le jeton d'actualisation invalidé échoue.

En bref, les jetons de rafraîchissement ont généralement une longue validité et sont toujours vérifiés par rapport au backend. Ils sont utilisés pour générer des jetons d'accès qui ont une validité très courte (quelques minutes). Ces jetons d'accès atteignent normalement votre backend mais vous ne vérifiez que leur signature et leur date d'expiration.

Ici, dans ma configuration, nous utilisons des jetons avec une validité plus longue (pouvant être des heures ou un jour), qui ont le même rôle et les mêmes fonctionnalités qu'un jeton d'accès et un jeton d'actualisation. Parce que nous avons mis en cache leur validation et invalidation par Nginx, ils ne sont entièrement vérifiés par le backend qu'une fois toutes les 5 minutes. Nous conservons donc l'avantage d'utiliser des jetons d'actualisation (pouvoir invalider rapidement un jeton) sans la complexité supplémentaire. Et la validation simple n'atteint jamais votre backend qui est au moins 1 ordre de grandeur plus lent que le cache Nginx, même s'il n'est utilisé que pour la signature et la vérification de la date d'expiration.

Avec cette configuration, je pouvais désactiver l'authentification dans mon backend, car toutes les demandes entrantes atteignent la auth_requestdirective Nginx avant de la toucher.

Cela ne résout pas complètement le problème si vous devez effectuer une autorisation par ressource, mais au moins vous avez enregistré la partie d'autorisation de base. Et vous pouvez même éviter de déchiffrer le jeton ou faire une recherche de base de données pour accéder aux données du jeton, car la réponse d'authentification en cache Nginx peut contenir des données et les transmettre au backend.

Maintenant, ma plus grande préoccupation est que je puisse briser quelque chose d'évident lié à la sécurité sans m'en rendre compte. Cela étant dit, tout jeton reçu est toujours validé au moins une fois avant d'être mis en cache par Nginx. Tout jeton tempéré serait différent et ne toucherait donc pas le cache puisque la clé de cache serait également différente.

En outre, il convient peut-être de mentionner qu'une authentification dans le monde réel lutterait contre le vol de jetons en générant (et en vérifiant) un Nonce supplémentaire ou quelque chose.

Voici un extrait simplifié de ma configuration Nginx pour mon application:

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

}

Maintenant, voici l'extrait de configuration pour le /authpoint de terminaison interne , inclus ci-dessus comme /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;
}

.

Traitement de la diffusion de contenu

Maintenant, l'authentification est séparée des données. Puisque vous avez dit qu'il était identique pour chaque utilisateur, le contenu lui-même peut également être mis en cache par Nginx (dans mon exemple, dans la content_cachezone).

Évolutivité

Ce scénario fonctionne très bien en supposant que vous avez un serveur Nginx. Dans un scénario réel, vous avez probablement une haute disponibilité, ce qui signifie plusieurs instances Nginx, potentiellement hébergeant également votre application dorsale (Laravel). Dans ce cas, toute demande de vos utilisateurs pourrait être envoyée à l'un de vos serveurs Nginx, et jusqu'à ce qu'ils aient tous mis en cache localement le jeton, ils continueront d'atteindre votre serveur pour le vérifier. Pour un petit nombre de serveurs, l'utilisation de cette solution apporterait tout de même de gros avantages.

Cependant, il est important de noter qu'avec plusieurs serveurs Nginx (et donc des caches), vous perdez la possibilité de vous déconnecter côté serveur car vous ne pouvez pas purger (en forçant une actualisation) le cache des jetons sur chacun d'eux, comme /auth/logoutfait dans mon exemple. Il ne vous reste plus que la durée du cache de jetons de 5 minutes qui forcera votre backend à être interrogé bientôt et indiquera à Nginx que la demande est refusée. Une solution de contournement partielle consiste à supprimer l'en-tête de jeton ou le cookie sur le client lors de la déconnexion.

Tout commentaire serait le bienvenu et apprécié!


Vous devriez recevoir beaucoup plus de votes positifs! Très utile, merci!
Gershon Papi

"J'ai ajouté quelques améliorations supplémentaires telles qu'un point de terminaison de déconnexion qui invalide un jeton en renvoyant 401 (qui est également mis en cache par Nginx) afin que si l'utilisateur clique sur déconnexion, le jeton ne puisse plus être utilisé même s'il n'est pas expiré. " - C'est intelligent! , mais est-ce que vous mettez sur liste noire le jeton dans votre backend également, de sorte que si le cache tombe en panne ou quelque chose, l'utilisateur ne peut toujours pas se connecter avec ce jeton particulier?
gaurav5430

"Cependant, il est important de noter qu'avec plusieurs serveurs Nginx (et donc des caches), vous perdez la possibilité de vous déconnecter côté serveur car vous ne pouvez pas purger (en forçant une actualisation) le cache des jetons sur chacun d'eux, comme / auth / logout dans mon exemple. " peux-tu élaborer?
gaurav5430
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.