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 internal
posizione 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-Control
direttiva 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_request
direttiva 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' /auth
endpoint 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_cache
zona).
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/logout
fa 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!