E, novamente, CAPTCHA ou nginx também sabe bordar

1. Introdução


Fui aqui para Habr e encontrei nos rascunhos um artigo não publicado sobre captcha, queria formalizá-lo e publicá-lo, mas decidi escrever um novo artigo alterando levemente o mecanismo e as ferramentas usadas. Na minha opinião, será útil ler um artigo antigo, não será pior.


O principal objetivo de escrever um novo artigo não é nem mostrar outro mecanismo de trabalho, quanto mostrar os recursos do nginx que às vezes são completamente esquecidos, considerando-o um servidor proxy banal.


Condições


Para impedir que os bots baixem arquivos, é usado um "captcha" de teste.


Ao formar um formulário para um salto de arquivo, uma imagem com um código e certas distorções é criada para complicar seu reconhecimento automático. Também há armazenamento para corrigir um par de chave + código para verificação.


Após a confirmação do formulário para baixar o arquivo e verificar o captcha quanto à correspondência do código, o arquivo é fornecido ao usuário ou um link único único para o arquivo é gerado. A exclusividade do link também é controlada pelo back-end. O par de chave + código também é removido para impedir sua reutilização.


Há um proxy que redireciona todas as solicitações para o back-end.


Os problemas


A geração complexa de imagens é uma operação que consome muitos recursos e, dado que nem todos os códigos mostrados são usados. É necessário criar um certo mecanismo para armazenar em cache imagens não utilizadas, a fim de poder exibi-las para outros usuários.


O código e a chave são verificados pelo back-end, mas há dificuldades em transferir arquivos grandes por meio do back-end, os links únicos também exigem verificação no nível do back-end. Quero me livrar da carga extra neles.


Solução


Selecionar funcionalidade


Na verdade, o captcha em si consiste em uma imagem e em uma determinada chave que corresponde ao código na imagem armazenada no back-end. A imagem não é muito grande e a traduzimos para Base64 e fornecemos um formulário:


<img src="data:image/png;base64,{{ IMAGE CODE BASE64 }}"> <input type="hidden" name="key" value="{{ KEY }}"> 

Ou JSON:


 { ... "data": { "image": "data:image/png;base64,{{ IMAGE CODE BASE64 }}", "key": "{{ KEY }}" } } 

Se temos uma parte do formulário sendo formada, podemos usar o SSI para inseri-la no corpo da página, para isso habilitamos o modo correspondente na configuração nginx no proxy:


 ssi on; 

E no código da página do formulário, inserimos:


 ... <form action="download" method="get" ...> ... <!--#include virtual="/x/captcha/generate"--> ... </form> ... 

Assim, alocamos a funcionalidade do mapeamento de captcha em locais ou métodos separados. Agora você pode fazer o cache.


Sim, o mecanismo SSI (Server Side Include) está quase esquecido, mas o módulo nginx é mais ativo que todos os vivos e funciona muito rapidamente. E, a propósito, se proxy_pass_cache armazena em cache a página inteira, o resultado da inclusão virtual não será armazenado em cache, mas será executado toda vez que for solicitado. Isso permite que você faça a inserção dinâmica.

CAPTCHA CAPTCHA


Para implementar o cache, precisamos de algo bastante aleatório e controlado pelo número de opções, a variável $ request_id é adequada para essa função - é bastante aleatória e hexadecimal, ou seja, ao escolher uma determinada parte dessa variável, você pode limitar o número de elementos do cache para 16 ^ n, em que n - o número de caracteres que precisamos extrair da variável. Então:


Determine a zona de cache:


 proxy_cache_path /cache/nginx/captcha levels=1:1 keys_zone=captcha:10m max_size=128m; 

É importante determinar qual valor de n escolhemos, respectivamente, os parâmetros dependem disso:


  • níveis = 1: 2
  • max_size = 128m
  • keys_zone = captcha: 10m

Então isso foi suficiente para tudo, mas não havia nada supérfluo. Em seguida, determinamos a chave de cache:


 server { ... set $captcha_salt 'salt'; if ( $request_id ~* "(\w{4})$" ) { set $cache_key $1; } ... 

A variável $ captcha_salt ainda é útil para nós, mas agora protege contra possíveis interseções de teclas. Eu escolhi n como 4, o que significa 16 ^ 4 slots de cache, com 2kb em média alocados a cada slot a partir do tamanho total do cache ( max_size = 128m ), o que deve ser suficiente, caso contrário você precisará aumentar o tamanho máximo.


Fazendo o local apropriado


 location /x/captcha/generate { proxy_cache captcha; proxy_cache_key "$captcha_salt:$cache_key"; proxy_cache_valid 200 365d; proxy_cache_valid any 0s; proxy_set_header Host "captcha.service.domain.my"; proxy_pass http://captcha_upstream/?cache_key=$cache_key; } 

As respostas "boas" de back-end serão armazenadas em cache quase para sempre, o restante não será armazenado em cache. E sim, você pode destacar imediatamente a funcionalidade de trabalhar com o captcha em um serviço separado.


A propósito, um mecanismo semelhante pode ser usado para gerar pseudo-dinâmica quando o usuário pressiona F5 e cada vez que uma nova “imagem” aleatória é mostrada a ele. Nesse caso, o back-end praticamente não está carregado.

Também precisamos redefinir o cache correspondente ao verificar o formulário, para que o back-end, entre outras coisas, precise fornecer o valor cache_key para transmiti-lo de volta ao formulário como um campo oculto . Infelizmente, a diretiva proxy_cache_purge está disponível apenas na versão comercial. Não importa, existe um módulo cache_purge de terceiros, que pode ser um pouco mais simples, mas o suficiente para nós. Portanto, local para liberar o cache:


 location /x/captcha/cache/purge { internal; proxy_cache_purge captcha "$captcha_salt:$arg_cache_key"; } 

Possui a diretiva interna , já que não vamos usá-la publicamente. E para chamar esse local, usaremos a diretiva mirror do módulo http_mirror_module :


Ou seja, fazemos uma solicitação paralela para redefinir o cache pela chave da variável $ arg_cache_key , que é transmitida no formulário. Em seguida, apenas copiei a solicitação ao nosso back-end, onde o restante do processamento é realizado.


A maneira espinhosa de otimização


Aqui, de fato, eu queria desenvolver um tópico: como separar a verificação do código captcha e o retorno do arquivo. Como impedir que o cache seja lavado com solicitações incorretas. Então, para otimizar cada vez mais, mas tudo se resume ao fato de que, em geral, não precisamos mais do back-end ... afinal ... porque já temos tudo.


A tarefa que permaneceu com o servidor em termos de verificação de captcha está na verdade verificando o código da chave + e removendo esse par do repositório. A verificação do código da tecla + pode ser uma comparação simples do valor md5 da chave. Para isso, basta um módulo para nós: http_secure_link_module . Ou seja, a chave pode ser representada como uma fórmula:


key = md5_baseurl( salt + code )

Ao mesmo tempo, a ligação ao slot do cache (chave do cache) não nos prejudicará, nós o adicionamos:


key = md5_baseurl( salt + code + cache_key )

Temos o Salt - esta é a variável $ captcha_salt (por isso foi útil), mas manter o sal em dois lugares no backend e proxy é ruim, então vamos fazer o seguinte:


 location /x/captcha/salt { allow {{ captcha backend IPs }}; deny all; return 200 "$captcha_salt"; } 

E deixe o back-end ir ao proxy para o sal.


A questão permanece no repositório, onde armazenamos um par de chave + código que precisa ser limpo. Para fazer isso, o mecanismo de cache que já implementamos é adequado para nós. A única coisa é que não processamos o resultado cache_purge de forma alguma , mas simplesmente espelhamos a solicitação, mas isso é corrigível. E sim, isso justifica o uso de uma chave de cache ao criar uma chave captcha.


Verificação de código


Reescreva os downloads de arquivos de localização :


 location /download { proxy_set_header Host $host; proxy_set_header X-Context download; proxy_set_header X-File-Name $arg_filename; proxy_set_header X-Key $arg_key; proxy_set_header X-Code $arg_code; proxy_set_header X-Cache-Key $arg_cache_key; proxy_pass http://127.0.0.1/x/captcha/check; proxy_intercept_errors on; error_page 403 404 = /download/fail; } 

Eu passo os parâmetros necessários com os cabeçalhos. Isso é opcional, mas é mais conveniente para mim. Proxy o processamento para o local local da verificação captcha. Além disso, context = download é passado, para que no manipulador possamos produzir um ou outro resultado, dependendo dele. Nesse caso, o manipulador pode retornar para nós:


  • 403 - erro de verificação de código. Na verdade, portanto, proxy_intercept_errors é incluído e um local é declarado para redirecionamento em caso de erro;
  • 404 - erro de limpeza do cache. O módulo cache_purge , se não houver nada no cache com essa chave, retorna 404;
  • 200 + Accel-Redirect - no local do upload do arquivo, caso a verificação captcha tenha corrido bem. No nosso caso, será o X-Accel-Redirect: / store / file

Se error_page pudesse lidar com códigos 2XX , então seria possível fazê-lo sozinho. Caso contrário, você precisará usar o mecanismo Accel-Redirect . Se você realmente deseja, pode separar os manipuladores de erro 403 e 404;

Cometer um erro de localização simples:


 location /download/fail { internal; return 200 "FAIL DOWNLOAD"; } 

Você pode devolver qualquer coisa neste local, dependendo de suas necessidades.


Estabelecemos o local do upload do arquivo:


 location /store/file { internal; add_header Content-Disposition "attachment; filename=\"$arg_filename\""; alias /spool/tmp/; try_files $arg_filename =404; } 

Primeiro, é importante que seja interno - isso significa que você não poderá fazer o download do arquivo diretamente através dele, apenas através do redirecionamento. Ele também pode ser modificado dependendo das necessidades e não distribuir o arquivo local, mas para proxy da solicitação do serviço de armazenamento de arquivos.


O seguinte local que temos para verificação de captcha:


 location /x/captcha/check { allow 127.0.0.1; deny all; secure_link_md5 "$captcha_salt$http_x_code$http_x_cache_key"; secure_link $http_x_key; if ($secure_link = "") { return 403 "FAIL CHECK CODE"; } proxy_set_header Host $host; proxy_pass http://127.0.0.1/x/captcha/purge; } 

Possui 2 blocos: verificação de código e proxy adicional para limpar o cache. Ao mesmo tempo, se a verificação do código não for aprovada, retorne imediatamente 403 (o texto não é importante, pois não é mais usado).


A procura em / x / captcha / purge retornará 2 opções de resposta:


  • 200 + Accel-Redirect - após a limpeza bem-sucedida do cache. O redirecionamento será para o X-Accel-Redirect: / x / captcha / check / ok ;
  • 404 - se não havia nada para limpar. Este resultado será passado acima para / download e será processado nele error_page ;

Um manipulador separado para a resposta positiva de / x / captcha / purge é criado devido ao fato de que primeiro precisamos alcançar um nível mais alto de proxy, e não entre / download e / x / captcha / check . Em segundo lugar, seria bom dar uma resposta positiva em relação ao contexto.


Vamos começar com um manipulador de respostas positivas:


 location /x/captcha/check/ok { internal; if ( $http_x_context = 'download' ) { add_header X-Accel-Redirect "/store/file?filename=$http_x_file_name"; } ... return 200 "OK"; } 

Na verdade, dependendo do valor da variável $ http_x_context (cabeçalho X-Context ), podemos determinar qual Accel-Redirect responderá com / x / captcha / check . Isso significa que você pode usar esse mecanismo em outros lugares, além de baixar o arquivo.


Limpar o cache é bastante simples:


 location /x/captcha/purge { allow 127.0.0.1; deny all; proxy_cache_purge captcha "$http_x_cache_key"; add_header X-Accel-Redirect "/x/captcha/check/ok"; } 

Em geral, é tudo, no final, temos a seguinte configuração nginx:


 proxy_cache_path /cache/nginx/captcha levels=1:1 keys_zone=captcha:10m max_size=128m; server { ... location /download { proxy_set_header Host $host; proxy_set_header X-Context download; proxy_set_header X-File-Name $arg_filename; proxy_set_header X-Key $arg_key; proxy_set_header X-Code $arg_code; proxy_set_header X-Cache-Key $arg_cache_key; proxy_pass http://127.0.0.1/x/captcha/check; proxy_intercept_errors on; error_page 403 404 = /download/fail; } location /download/fail { internal; return 200 "FAIL DOWNLOAD"; } location /store/file { internal; add_header Content-Disposition "attachment; filename=\"$arg_filename\""; alias /spool/tmp/; try_files $arg_filename =404; } ... set $captcha_salt 'salt'; if ( $request_id ~* "(\w{4})$" ) { set $cache_key $1; } location /x/captcha/generate { proxy_cache captcha; proxy_cache_key "$captcha_salt:$cache_key"; proxy_cache_valid 200 365d; proxy_cache_valid any 0s; proxy_set_header Host "captcha.service.domain.my"; proxy_pass http://captcha_upstream/?cache_key=$cache_key; } location /x/captcha/salt { allow {{ captcha backend IPs }}; deny all; return 200 "$captcha_salt"; } location /x/captcha/check { allow 127.0.0.1; deny all; secure_link_md5 "$captcha_salt$http_x_code$http_x_cache_key"; secure_link $http_x_key; if ($secure_link = "") { return 403 "FAIL CHECK CODE"; } proxy_set_header Host $host; proxy_pass http://127.0.0.1/x/captcha/purge; } location /x/captcha/check/ok { internal; if ( $http_x_context = 'download' ) { add_header X-Accel-Redirect "/store/file?filename=$http_x_file_name"; } ... return 200 "OK"; } location /x/captcha/purge { allow 127.0.0.1; deny all; proxy_cache_purge captcha "$http_x_cache_key"; add_header X-Accel-Redirect "/x/captcha/check/ok"; } } 

O que você deve prestar atenção:

  • O Accel-Redirect funciona apenas quando o status da resposta é 2XX. É verdade que, infelizmente, nada foi escrito sobre isso em nenhum lugar, e os adeptos do nginx discordam;
  • Locais privados próximos permitem 127.0.0.1; negar tudo; seja interno; , dependendo de chegarmos a esse local por proxy_pass ou por Accel-Redirect ;
  • Todos os locais associados ao captcha são destacados em / x / capcha / ... para que seja possível formar um microsserviço;

Para maior clareza, também desenhei um diagrama do trabalho:


imagem

Sumário


Como resultado, a partir do back-end, precisamos apenas gerar diretamente a imagem e o código para ela. O Nginx pode lidar facilmente com o resto. Obviamente, essas são operações lógicas relativamente simples, mas, no entanto, isso acelerará significativamente o trabalho e reduzirá a carga no back-end. De fato, não usamos mecanismos incomuns, mas apenas:


  • proxy_cache;
  • Acelerar-Redirecionar
  • error_page;
  • secure_link
  • cache_purge;

O resto é a construção correta de cadeias lógicas.


Também removemos o repositório de back-end temporário para códigos e links únicos. No entanto, eles criaram um elemento obrigatório do sistema nginx, aumentando seu peso funcional.

Source: https://habr.com/ru/post/pt485804/


All Articles