Almacenamiento en caché de solicitudes autenticadas para todos los usuarios

9

Estoy trabajando en una aplicación web que debe lidiar con impulsos muy grandes de usuarios simultáneos, que necesitan autorización, para solicitar contenido idéntico. En su estado actual, es totalmente paralizante incluso para una instancia de AWS de 32 núcleos.

(Tenga en cuenta que estamos usando Nginx como proxy inverso)

La respuesta no puede simplemente almacenarse en caché ya que, en el peor de los casos, debemos verificar si el usuario está autenticado decodificando su JWT. Esto requiere que activemos Laravel 4, que la mayoría estaría de acuerdo, es lento , incluso con PHP-FPM y OpCache habilitados. Esto se debe principalmente a la fuerte fase de arranque.

Uno podría hacer la pregunta "¿Por qué usaste PHP y Laravel en primer lugar si sabías que esto iba a ser un problema?" ¡Pero ya es demasiado tarde para volver a tomar esa decisión!

Solución posible

Una solución que se ha presentado es extraer el módulo Auth de Laravel a un módulo externo liviano (escrito en algo rápido como C) cuya responsabilidad es decodificar el JWT y decidir si el usuario está autenticado.

El flujo de una solicitud sería:

  1. Verifique si la memoria caché golpeó (si no pasa a PHP de manera normal)
  2. Decodifica la ficha
  3. Comprueba si es válido
  4. Si es válido , servir desde caché
  5. Si no es válido , dígale a Nginx, y luego Nginx pasará la solicitud a PHP para que lo atienda de manera normal.

Esto nos permitirá no acceder a PHP una vez que hayamos enviado esta solicitud a un solo usuario y, en cambio, llegar a un módulo liviano para jugar con la decodificación de JWT y cualquier otra advertencia que venga con este tipo de autenticación.

Incluso estaba pensando en escribir este código directamente como un módulo de extensión Nginx HTTP.

Preocupaciones

Mi preocupación es que nunca había visto esto antes y me preguntaba si había una mejor manera.

Además, en el momento en que agrega cualquier contenido específico del usuario a la página, elimina totalmente este método.

¿Hay otra solución más simple disponible directamente en Nginx? ¿O tendríamos que usar algo más especializado como el barniz?

Mis preguntas:

¿Tiene sentido la solución anterior?

¿Cómo se aborda esto normalmente?

¿Hay una mejor manera de lograr un aumento de rendimiento similar o mejor?

iamyojimbo
fuente
Estoy lidiando con un problema similar. Un par de ideas a) Nginx auth_request puede transferirlo a su microservicio de autenticación, aliviando la necesidad de desarrollar un módulo Nginx. b) Alternativamente, su microservicio podría redirigir a los usuarios autenticados a una URL temporal que sea pública, almacenable en caché e incuestionable, pero que el servidor de PHP pueda validar para que sea válida por un período limitado (el período de caché). Esto sacrifica algo de seguridad, si la URL temporal se filtra a un usuario que no es de confianza, puede acceder al contenido durante ese período limitado, al igual que un token OAuth Bearer.
James
¿Se te ocurrió una solución para esto? Estoy enfrentando lo mismo
timbroder
Resulta que al tener un gran grupo de nodos de back-end optimizados, pudimos lidiar con la carga, pero tengo mucha confianza en que este enfoque sea una gran solución de ahorro de costos a largo plazo. Si conoce algunas de las respuestas que podría estar atendiendo por adelantado, si calienta el caché antes de la afluencia de solicitudes, el ahorro de recursos de fondo y la ganancia de confiabilidad serían muy altos.
iamyojimbo

Respuestas:

9

He estado tratando de abordar un problema similar. Mis usuarios deben autenticarse para cada solicitud que realicen. Me he centrado en hacer que los usuarios se autentiquen al menos una vez mediante la aplicación de back-end (validación del token JWT), pero después de eso, decidí que ya no necesitaría el backend.

Elegí evitar requerir cualquier complemento de Nginx que no esté incluido por defecto. De lo contrario, puede verificar nginx-jwt o Lua scripting y estas probablemente serían excelentes soluciones.

Direccionamiento de autenticación

Hasta ahora he hecho lo siguiente:

  • Delegó la autenticación a Nginx usando auth_request. Esto llama a una internalubicación que pasa la solicitud a mi punto final de validación de token de back-end. Esto por sí solo no aborda el problema de manejar una gran cantidad de validaciones en absoluto.

  • El resultado de la validación del token se almacena en caché mediante una proxy_cache_key "$cookie_token";directiva. Tras la validación exitosa del token, el backend agrega una Cache-Controldirectiva que le dice a Nginx que solo guarde en caché el token por hasta 5 minutos. En este punto, cualquier token de autenticación validado una vez está en el caché, ¡las solicitudes posteriores del mismo usuario / token ya no tocan el backend de autenticación!

  • Para proteger mi aplicación back-end contra posibles inundaciones por tokens no válidos, también cacheé las validaciones rechazadas, cuando mi punto final backend devuelve 401. Estas solo se almacenan en caché por un corto período de tiempo para evitar potencialmente llenar el caché Nginx con tales solicitudes.

He agregado un par de mejoras adicionales, como un punto final de cierre de sesión que invalida un token al devolver 401 (que también está en caché por Nginx) para que si el usuario hace clic en cerrar sesión, el token ya no se puede usar, incluso si no ha expirado.

Además, mi caché Nginx contiene para cada token, el usuario asociado como un objeto JSON, lo que me evita recuperarlo de la base de datos si necesito esta información; y también me salva de descifrar el token.

Acerca del token de por vida y los tokens de actualización

Después de 5 minutos, el token habrá expirado en el caché, por lo que se volverá a consultar el backend. Esto es para asegurarse de que puede invalidar un token, porque el usuario cierra sesión, porque se ha visto comprometido, etc. Dicha revalidación periódica, con una implementación adecuada en el back-end, me evita tener que usar tokens de actualización.

Tradicionalmente, los tokens de actualización se usarían para solicitar un nuevo token de acceso; se almacenarían en su backend y usted verificaría que una solicitud de un token de acceso se realice con un token de actualización que coincida con el que tiene en la base de datos para este usuario específico. Si el usuario cierra la sesión, o los tokens se ven comprometidos, eliminará / invalidará el token de actualización en su base de datos para que la próxima solicitud de un nuevo token que use el token de actualización invalidado falle.

En resumen, los tokens de actualización generalmente tienen una validez larga y siempre se verifican en el backend. Se utilizan para generar tokens de acceso que tienen una validez muy corta (unos minutos). Estos tokens de acceso normalmente llegan a su backend, pero solo verifica su firma y fecha de vencimiento.

Aquí, en mi configuración, estamos usando tokens con una validez más larga (pueden ser horas o un día), que tienen el mismo rol y características que un token de acceso y un token de actualización. Debido a que Nginx tiene su validación e invalidación en caché, el backend solo las verifica completamente una vez cada 5 minutos. Por lo tanto, mantenemos el beneficio de usar tokens de actualización (poder invalidar rápidamente un token) sin la complejidad adicional. Y la validación simple nunca llega a su backend que es al menos 1 orden de magnitud más lenta que la caché Nginx, incluso si se usa solo para la firma y la verificación de la fecha de vencimiento.

Con esta configuración, podría deshabilitar la autenticación en mi backend, ya que todas las solicitudes entrantes llegan a la auth_requestdirectiva Nginx antes de tocarla.

Eso no resuelve completamente el problema si necesita realizar algún tipo de autorización por recurso, pero al menos ha guardado la parte de autorización básica. E incluso puede evitar descifrar el token o hacer una búsqueda de base de datos para acceder a los datos del token ya que la respuesta de autenticación en caché de Nginx puede contener datos y devolverlos al back-end.

Ahora, mi mayor preocupación es que puedo estar rompiendo algo obvio relacionado con la seguridad sin darme cuenta. Dicho esto, cualquier token recibido todavía se valida al menos una vez antes de que Nginx lo almacene en caché. Cualquier token templado sería diferente, por lo que no golpearía el caché ya que la clave del caché también sería diferente.

Además, tal vez vale la pena mencionar que una autenticación del mundo real lucharía contra el robo de tokens al generar (y verificar) un Nonce adicional o algo así.

Aquí hay un extracto simplificado de mi configuración de Nginx para mi aplicación:

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

}

Ahora, aquí está el extracto de configuración para el /authpunto final interno , incluido anteriormente como /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;
}

.

Abordar la publicación de contenido

Ahora la autenticación está separada de los datos. Como dijiste que era idéntico para cada usuario, el contenido en sí también puede ser almacenado en caché por Nginx (en mi ejemplo, en la content_cachezona).

Escalabilidad

Este escenario funciona de manera inmediata, suponiendo que tenga un servidor Nginx. En un escenario del mundo real, es probable que tenga una alta disponibilidad, lo que significa múltiples instancias de Nginx, y posiblemente también aloje su aplicación de back-end (Laravel). En ese caso, cualquier solicitud que hagan sus usuarios podría enviarse a cualquiera de sus servidores Nginx, y hasta que todos hayan almacenado en caché localmente el token, seguirán llegando a su servidor para verificarlo. Para una pequeña cantidad de servidores, el uso de esta solución aún traería grandes beneficios.

Sin embargo, es importante tener en cuenta que con varios servidores Nginx (y, por lo tanto, cachés), pierde la capacidad de cerrar sesión en el lado del servidor porque no puede purgar (forzando una actualización) el caché de tokens en todos ellos, como /auth/logoutlo hace en mi ejemplo. Solo le queda la duración de la memoria caché de token de 5mn que obligará a consultar su backend pronto y le dirá a Nginx que la solicitud ha sido denegada. Una solución parcial es eliminar el encabezado del token o la cookie en el cliente al cerrar sesión.

Cualquier comentario sería muy bienvenido y apreciado!

mbarthelemy
fuente
¡Deberías recibir muchos más votos a favor! Muy útil, gracias!
Gershon Papi
"He agregado un par de mejoras adicionales, como un punto final de cierre de sesión que invalida un token al devolver 401 (que también es almacenado en caché por Nginx) para que si el usuario hace clic en cerrar sesión, el token ya no se puede usar, incluso si no ha expirado. " - Esto es inteligente! , pero ¿también estás en la lista negra del token en tu back-end, de modo que en caso de que el caché se caiga o algo así, el usuario aún no puede iniciar sesión con ese token en particular?
gaurav5430
"Sin embargo, es importante tener en cuenta que con varios servidores Nginx (y, por lo tanto, cachés), pierde la capacidad de cerrar sesión en el lado del servidor porque no puede purgar (forzando una actualización) el caché de tokens en todos ellos, como / auth / logout hace en mi ejemplo ". ¿puedes elaborar?
gaurav5430