Memorizzazione nella cache delle richieste autenticate per tutti gli utenti


9

Sto lavorando a un'app Web che deve gestire impulsi molto ampi di utenti simultanei, che devono essere autorizzati, per richiedere contenuti identici. Allo stato attuale, è totalmente paralizzante anche per un'istanza AWS a 32 core.

(Nota che stiamo usando Nginx come proxy inverso)

La risposta non può essere semplicemente memorizzata nella cache poiché, nel peggiore dei casi, dobbiamo verificare se l'utente è autenticato decodificando il proprio JWT. Ciò richiede che si accenda Laravel 4, che la maggior parte sarebbe d'accordo, è lento , anche con PHP-FPM e OpCache abilitati. Ciò è dovuto principalmente alla pesante fase di bootstrap.

Si potrebbe porre la domanda "Perché hai usato PHP e Laravel in primo luogo se sapevi che questo sarebbe stato un problema?" - ma è troppo tardi ora per tornare su quella decisione!

Possibile soluzione

Una soluzione che è stata proposta è quella di estrarre il modulo Auth da Laravel in un modulo esterno leggero (scritto in qualcosa di veloce come C) la cui responsabilità è decodificare il JWT e decidere se l'utente è autenticato.

Il flusso di una richiesta sarebbe:

  1. Controlla se la cache ha colpito (se non passa a PHP normalmente)
  2. Decodifica il token
  3. Controlla se è valido
  4. Se valido , servire dalla cache
  5. Se non valido , informa Nginx, quindi Nginx passerà quindi la richiesta a PHP per gestirlo normalmente.

Questo ci permetterà di non colpire PHP una volta che abbiamo inviato questa richiesta a un singolo utente e invece di raggiungere un modulo leggero per scherzare con la decodifica JWT e qualsiasi altro avvertimento fornito con questo tipo di autenticazione.

Stavo persino pensando di scrivere questo codice direttamente come modulo di estensione HTTP Nginx.

preoccupazioni

La mia preoccupazione è che non l'ho mai visto prima e mi chiedevo se esiste un modo migliore.

Inoltre, nel momento in cui aggiungi qualsiasi contenuto specifico dell'utente alla pagina, questo metodo viene completamente eliminato.

C'è un'altra soluzione più semplice disponibile direttamente in Nginx? O dovremmo usare qualcosa di più specializzato come Varnish?

Le mie domande:

La soluzione sopra ha senso?

Come viene normalmente affrontato?

Esiste un modo migliore per ottenere un rendimento simile o migliore?


Sono alle prese con un problema simile. Un paio di idee a) Nginx auth_request potrebbe essere in grado di consegnare al microservizio di autenticazione, alleviando la necessità di sviluppare un modulo Nginx. b) In alternativa, il microservizio potrebbe reindirizzare gli utenti autenticati a un URL temporaneo che è pubblico, memorizzabile nella cache e non indovinabile, ma può essere convalidato dal back-end di PHP per essere valido per un periodo limitato (il periodo della cache). Ciò sacrifica un po 'di sicurezza, se l'URL temporaneo viene trapelato a un utente non attendibile, può accedere al contenuto per quel periodo limitato, proprio come un token OAuth Bearer.
James,

Hai trovato una soluzione a questo? Sto affrontando la stessa cosa
timbroder

Si scopre che disponendo di un ampio cluster di nodi backend ottimizzati, siamo stati in grado di gestire il carico, ma ho la massima fiducia in questo approccio, essendo una soluzione di grande risparmio a lungo termine. Se conosci alcune delle risposte che potresti fornire in anticipo, se riscaldi la cache prima dell'afflusso di richieste, il risparmio delle risorse di back-end e il guadagno di affidabilità sarebbero molto alti.
iamyojimbo,

Risposte:


9

Ho provato a risolvere un problema simile. I miei utenti devono essere autenticati per ogni richiesta che fanno. Mi sono concentrato su come ottenere l'autenticazione degli utenti almeno una volta dall'app back-end (convalida del token JWT), ma dopo ciò, ho deciso che non avrei più bisogno del back-end.

Ho scelto di evitare di richiedere qualsiasi plug-in Nginx che non è incluso per impostazione predefinita. Altrimenti puoi controllare lo script di nginx-jwt o Lua e queste sarebbero probabilmente ottime soluzioni.

Indirizzamento dell'autenticazione

Finora ho fatto quanto segue:

  • Delega dell'autenticazione a Nginx utilizzando auth_request. Questo chiama una internalposizione che passa la richiesta al mio endpoint di convalida del token back-end. Questo da solo non affronta il problema della gestione di un numero elevato di convalide.

  • Il risultato della convalida del token viene memorizzato nella cache utilizzando una proxy_cache_key "$cookie_token";direttiva. Se la convalida del token riesce, il backend aggiunge una Cache-Controldirettiva che dice a Nginx di memorizzare nella cache il token solo per un massimo di 5 minuti. A questo punto, qualsiasi token di autenticazione convalidato una volta è nella cache, le richieste successive dallo stesso utente / token non toccano più il backend di autenticazione!

  • Per proteggere la mia app back-end da potenziali inondazioni da token non validi, memorizzo anche nella cache le convalide rifiutate, quando il mio endpoint back-end restituisce 401. Questi vengono memorizzati nella cache solo per una breve durata per evitare di riempire potenzialmente la cache Nginx con tali richieste.

Ho aggiunto un paio di miglioramenti aggiuntivi come un endpoint di logout che invalida un token restituendo 401 (che è anche memorizzato nella cache da Nginx) in modo che se l'utente fa clic sul logout, il token non può più essere utilizzato anche se non è scaduto.

Inoltre, la mia cache Nginx contiene per ogni token, l'utente associato come oggetto JSON, che mi salva dal recupero dal DB se ho bisogno di queste informazioni; e mi salva anche dalla decrittazione del token.

Informazioni sulla durata dei token e sui token di aggiornamento

Dopo 5 minuti, il token sarà scaduto nella cache, quindi il back-end verrà nuovamente interrogato. Questo per assicurarti di essere in grado di invalidare un token, perché l'utente si disconnette, perché è stato compromesso e così via. Tale rinnovo periodico, con una corretta implementazione nel back-end, mi evita di utilizzare i token di aggiornamento.

I token di aggiornamento tradizionali verrebbero utilizzati per richiedere un nuovo token di accesso; verrebbero archiviati nel tuo back-end e verifichi che una richiesta per un token di accesso viene effettuata con un token di aggiornamento che corrisponde a quello presente nel database per questo specifico utente. Se l'utente si disconnette o i token sono compromessi, è necessario eliminare / invalidare il token di aggiornamento nel DB in modo che la successiva richiesta di un nuovo token che utilizza il token di aggiornamento non valido non abbia esito positivo.

In breve, i token di aggiornamento in genere hanno una validità lunga e vengono sempre verificati rispetto al back-end. Sono utilizzati per generare token di accesso che hanno una validità molto breve (pochi minuti). Questi token di accesso normalmente raggiungono il tuo back-end ma controlli solo la loro firma e la data di scadenza.

Qui nella mia configurazione, stiamo usando token con una validità più lunga (può essere ore o un giorno), che hanno lo stesso ruolo e le stesse funzionalità di un token di accesso e di un token di aggiornamento. Poiché la loro convalida e invalidazione sono memorizzate nella cache da Nginx, vengono verificate completamente dal back-end solo una volta ogni 5 minuti. Quindi manteniamo il vantaggio di utilizzare i token di aggiornamento (essere in grado di invalidare rapidamente un token) senza la complessità aggiunta. E la semplice convalida non raggiunge mai il tuo backend che è almeno 1 ordine di grandezza più lento della cache di Nginx, anche se usato solo per il controllo della firma e della data di scadenza.

Con questa configurazione, potrei disabilitare l'autenticazione nel mio backend, poiché tutte le richieste in arrivo raggiungono la auth_requestdirettiva Nginx prima di toccarla.

Ciò non risolve completamente il problema se è necessario eseguire qualsiasi tipo di autorizzazione per risorsa, ma almeno è stata salvata la parte di autorizzazione di base. E puoi anche evitare di decrittografare il token o eseguire una ricerca DB per accedere ai dati del token poiché la risposta di autenticazione memorizzata nella cache di Nginx può contenere dati e passarli al back-end.

Ora, la mia più grande preoccupazione è che potrei rompere qualcosa di ovvio legato alla sicurezza senza accorgermene. Detto questo, ogni token ricevuto viene comunque convalidato almeno una volta prima di essere memorizzato nella cache da Nginx. Qualsiasi token temperato sarebbe diverso, quindi non colpirebbe la cache poiché anche la chiave della cache sarebbe diversa.

Inoltre, forse vale la pena ricordare che un'autenticazione del mondo reale dovrebbe combattere il furto di token generando (e verificando) un Nonce aggiuntivo o qualcosa del genere.

Ecco un estratto semplificato della mia configurazione Nginx per la mia 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;
    }

}

Ora, ecco l'estratto di configurazione per l' /authendpoint interno , incluso sopra come /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;
}

.

Affrontare la pubblicazione di contenuti

Ora l'autenticazione è separata dai dati. Poiché hai detto che era identico per ogni utente, il contenuto stesso può anche essere memorizzato nella cache da Nginx (nel mio esempio, nella content_cachezona).

scalabilità

Questo scenario funziona alla grande, supponendo che tu abbia un server Nginx. In uno scenario del mondo reale probabilmente hai un'alta disponibilità, ovvero più istanze di Nginx, che potenzialmente ospita anche la tua applicazione back-end (Laravel). In tal caso, qualsiasi richiesta fatta dagli utenti potrebbe essere inviata a qualsiasi server Nginx e fino a quando tutti non avranno memorizzato nella cache localmente il token, continueranno a contattare il back-end per verificarlo. Per un numero limitato di server, l'utilizzo di questa soluzione porterebbe comunque grandi vantaggi.

Tuttavia, è importante notare che con più server Nginx (e quindi cache) si perde la possibilità di disconnettersi sul lato server perché non si è in grado di eliminare (forzando un aggiornamento) la cache dei token su tutti loro, come /auth/logoutfa nel mio esempio. Ti resta solo la durata della cache del token 5mn che imporrà presto la query del back-end e dirà a Nginx che la richiesta è stata respinta. Una soluzione parziale consiste nell'eliminare l'intestazione del token o il cookie sul client durante la disconnessione.

Qualsiasi commento sarebbe molto gradito e apprezzato!


Dovresti ricevere molti più voti! Molto utile, grazie!
Gershon Papi,

"Ho aggiunto un paio di miglioramenti aggiuntivi come un endpoint di logout che invalida un token restituendo 401 (che è anche memorizzato nella cache da Nginx) in modo che se l'utente fa clic sul logout, il token non può più essere utilizzato anche se non è scaduto. " - È intelligente! , ma stai effettivamente inserendo nella blacklist il token anche nel tuo back-end, in modo che nel caso in cui la cache si blocchi o qualcosa del genere, l'utente non è ancora in grado di accedere con quel particolare token?
gaurav5430,

"Tuttavia, è importante notare che con più server Nginx (e quindi cache) si perde la possibilità di disconnettersi sul lato server perché non è possibile eliminare (forzando un aggiornamento) la cache dei token su tutti, come nel mio esempio / auth / logout. " Puoi elaborare?
gaurav5430,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.