Armazenando em cache solicitações autenticadas para todos os usuários

9

Estou trabalhando em um aplicativo Web que deve lidar com impulsos muito grandes de usuários simultâneos, que precisam ser autorizados, para solicitar conteúdo idêntico. No seu estado atual, é totalmente incapacitante mesmo para uma instância da AWS de 32 núcleos.

(Observe que estamos usando o Nginx como um proxy reverso)

A resposta não pode ser simplesmente armazenada em cache, pois, na pior das hipóteses, devemos verificar se o usuário está autenticado decodificando seu JWT. Isso exige que a inicialização do Laravel 4, o que muitos concordam, seja lenta , mesmo com o PHP-FPM e o OpCache ativados. Isso se deve principalmente à pesada fase de inicialização.

Alguém poderia fazer a pergunta "Por que você usou PHP e Laravel em primeiro lugar, se sabia que isso seria um problema?" - mas agora é tarde demais para voltarmos a essa decisão!

Solução possível

Uma solução que foi apresentada é extrair o módulo Auth do Laravel para um módulo externo leve (escrito em algo rápido como C) cuja responsabilidade é decodificar o JWT e decidir se o usuário é autenticado.

O fluxo de uma solicitação seria:

  1. Verifique se o cache foi atingido (caso contrário, não passe para o PHP normalmente)
  2. Decodificar o token
  3. Verifique se é válido
  4. Se válido , sirva a partir do cache
  5. Se inválido , informe ao Nginx e o Nginx passará a solicitação para o PHP para lidar normalmente.

Isso nos permitirá não acessar o PHP depois de atendermos a essa solicitação a um único usuário e, em vez disso, procurar um módulo leve para mexer com a decodificação de JWTs e quaisquer outras advertências que acompanham esse tipo de autenticação.

Eu estava pensando em escrever esse código diretamente como um módulo de extensão HTTP Nginx.

Preocupações

Minha preocupação é que nunca vi isso antes e me perguntei se havia uma maneira melhor.

Além disso, no momento em que você adiciona qualquer conteúdo específico do usuário à página, ele mata totalmente esse método.

Existe outra solução mais simples disponível diretamente no Nginx? Ou teríamos que usar algo mais especializado como o verniz?

Minhas perguntas:

A solução acima faz sentido?

Como isso é normalmente abordado?

Existe uma maneira melhor de obter um ganho de desempenho semelhante ou melhor?

iamyojimbo
fonte
Estou lidando com um problema semelhante. Algumas idéias a) O Nginx auth_request pode entregar ao seu microsserviço de autenticação, aliviando a necessidade de desenvolver um módulo Nginx. b) Como alternativa, seu microsserviço pode redirecionar usuários autenticados para um URL temporário que é público, armazenável em cache e impossível de adivinhar, mas pode ser validado pelo back-end do PHP como válido por um período limitado (o período do cache). Isso sacrifica um pouco de segurança; se o URL temporário vazar para um usuário não confiável, ele poderá acessar o conteúdo por um período limitado, como um token de portador OAuth.
James
Você encontrou uma solução para isso? Estou enfrentando a mesma coisa
timbroder
Acontece que, ao termos um grande cluster de nós de back-end otimizados, conseguimos lidar com a carga - mas tenho muita confiança nessa abordagem, sendo uma solução de grande economia de custos a longo prazo. Se você conhece algumas das respostas que poderia fornecer com antecedência, se aquecer o cache antes do influxo de solicitações, a economia de recursos de back-end e o ganho de confiabilidade serão muito altos.
Iamyojimbo

Respostas:

9

Eu tenho tentado resolver um problema semelhante. Meus usuários precisam ser autenticados para cada solicitação que fazem. Eu tenho me concentrado em conseguir que os usuários sejam autenticados pelo menos uma vez pelo aplicativo de back-end (validação do token JWT), mas depois disso, decidi que não precisava mais do back-end.

Eu escolhi evitar exigir qualquer plug-in Nginx que não esteja incluído por padrão. Caso contrário, você pode verificar os scripts nginx-jwt ou Lua e essas provavelmente seriam ótimas soluções.

Endereçando autenticação

Até agora, eu fiz o seguinte:

  • Delegou a autenticação ao Nginx usando auth_request. Isso chama um internallocal que transmite a solicitação ao meu terminal de validação de token de back-end. Isso por si só não aborda a questão de lidar com um alto número de validações.

  • O resultado da validação do token é armazenado em cache usando uma proxy_cache_key "$cookie_token";diretiva. Após a validação bem-sucedida do token, o back-end adiciona uma Cache-Controldiretiva que informa ao Nginx que apenas armazene em cache o token por até 5 minutos. Nesse ponto, qualquer token de autenticação validado uma vez está no cache, solicitações subsequentes do mesmo usuário / token não tocam mais no back-end de autenticação!

  • Para proteger meu aplicativo de back-end contra possíveis inundações por tokens inválidos, eu também armazeno em cache as validações recusadas quando meu ponto de extremidade de back-end retorna 401. Esses são armazenados em cache apenas por um curto período para evitar o preenchimento potencial do cache do Nginx com essas solicitações.

Adicionei algumas melhorias adicionais, como um ponto de extremidade de logoff que invalida um token retornando 401 (que também é armazenado em cache pelo Nginx) para que, se o usuário clicar em logout, o token não possa mais ser usado, mesmo que não tenha expirado.

Além disso, meu cache Nginx contém para cada token, o usuário associado como um objeto JSON, o que me impede de buscá-lo no banco de dados se eu precisar dessas informações; e também me impede de descriptografar o token.

Sobre a vida útil do token e atualizar tokens

Após 5 minutos, o token expirará no cache, portanto, o back-end será consultado novamente. Isso é para garantir que você consiga invalidar um token, porque o usuário efetua logout, porque foi comprometido e assim por diante. Essa revalidação periódica, com a implementação adequada no back-end, evita que eu precise usar tokens de atualização.

Tradicionalmente, os tokens de atualização seriam usados ​​para solicitar um novo token de acesso; eles seriam armazenados no seu back-end e você verificaria se uma solicitação de um token de acesso é feita com um token de atualização que corresponda ao que você possui no banco de dados para esse usuário específico. Se o usuário efetuar logout ou tokens forem comprometidos, você excluirá / invalidará o token de atualização no seu banco de dados para que a próxima solicitação de um novo token usando o token de atualização invalidado falhe.

Em resumo, os tokens de atualização geralmente têm uma validade longa e são sempre verificados no back-end. Eles são usados ​​para gerar tokens de acesso com uma validade muito curta (alguns minutos). Esses tokens de acesso normalmente atingem seu back-end, mas você apenas verifica a assinatura e a data de validade.

Aqui na minha configuração, estamos usando tokens com uma validade mais longa (pode ser horas ou um dia), que têm a mesma função e recursos que um token de acesso e um token de atualização. Como temos sua validação e invalidação armazenadas em cache pelo Nginx, elas são verificadas totalmente pelo back-end apenas a cada 5 minutos. Portanto, mantemos o benefício de usar tokens de atualização (poder invalidar rapidamente um token) sem a complexidade adicional. E a validação simples nunca chega ao seu back-end que é pelo menos uma ordem de magnitude mais lenta que o cache do Nginx, mesmo se usado apenas para verificação de assinatura e data de validade.

Com essa configuração, eu pude desativar a autenticação no meu back-end, pois todas as solicitações recebidas atingem a auth_requestdiretiva Nginx antes de tocá-la.

Isso não resolve completamente o problema se você precisar executar qualquer tipo de autorização por recurso, mas pelo menos você salvou a parte básica da autorização. E você pode até evitar descriptografar o token ou fazer uma pesquisa no banco de dados para acessar os dados do token, pois a resposta de autenticação em cache do Nginx pode conter dados e transmiti-los ao back-end.

Agora, minha maior preocupação é que eu possa estar quebrando algo óbvio relacionado à segurança sem perceber. Dito isto, qualquer token recebido ainda é validado pelo menos uma vez antes de ser armazenado em cache pelo Nginx. Qualquer token temperado seria diferente, portanto, não atingiria o cache, pois a chave do cache também seria diferente.

Além disso, talvez valha a pena mencionar que uma autenticação do mundo real lutaria contra o roubo de token gerando (e verificando) um Nonce adicional ou algo assim.

Aqui está um extrato simplificado da minha configuração do Nginx para o meu aplicativo:

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

}

Agora, aqui está o extrato de configuração para o /authterminal interno , incluído acima 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;
}

.

Endereçando a veiculação de conteúdo

Agora a autenticação está separada dos dados. Como você disse que era idêntico para todos os usuários, o próprio conteúdo também pode ser armazenado em cache pelo Nginx (no meu exemplo, na content_cachezona).

Escalabilidade

Esse cenário funciona muito bem desde que você tenha um servidor Nginx. Em um cenário do mundo real, você provavelmente tem alta disponibilidade, o que significa várias instâncias do Nginx, potencialmente também hospedando seu aplicativo de back-end (Laravel). Nesse caso, qualquer solicitação que seus usuários fizerem poderá ser enviada para qualquer um dos servidores Nginx e, até que todos tenham armazenado em cache localmente o token, eles continuarão alcançando seu back-end para verificá-lo. Para um pequeno número de servidores, o uso dessa solução ainda traria grandes benefícios.

No entanto, é importante observar que, com vários servidores Nginx (e, portanto, caches), você perde a capacidade de efetuar logoff no lado do servidor porque não pode limpar (forçando uma atualização) o cache de tokens em todos eles, como /auth/logoutfaz no meu exemplo. Você só tem a duração do cache de token de 5mn que forçará seu back-end a ser consultado em breve e informará ao Nginx que a solicitação foi negada. Uma solução parcial é excluir o cabeçalho ou o cookie do token no cliente ao fazer logout.

Qualquer comentário seria muito bem-vindo e apreciado!

mbarthelemy
fonte
Você deveria receber muito mais votos! Muito útil, obrigado!
Gershon Papi
"Adicionei algumas melhorias adicionais, como um ponto de extremidade de logoff que invalida um token retornando 401 (que também é armazenado em cache pelo Nginx) para que, se o usuário clicar em logout, o token não possa mais ser usado, mesmo que não tenha expirado. " - Isso é inteligente! , mas você também está na lista negra do token no back-end, para que, caso o cache caia ou algo assim, o usuário ainda não consiga efetuar login com esse token específico?
gaurav5430
"No entanto, é importante observar que, com vários servidores Nginx (e, portanto, caches), você perde a capacidade de fazer logoff no lado do servidor porque não pode limpar (forçando uma atualização) o cache de tokens em todos eles, como / auth / logout faz no meu exemplo. " Você pode elaborar?
gaurav5430