Y de nuevo, captcha o nginx también sabe bordar

Introduccion


Fui a Habr y encontré en los borradores un artículo inédito sobre captcha, quería formalizarlo y publicarlo, pero decidí escribir un nuevo artículo cambiando ligeramente el mecanismo y las herramientas utilizadas. En mi opinión, será útil leer un artículo antiguo, no será peor.


El objetivo principal de escribir un nuevo artículo es ni siquiera mostrar otro mecanismo de trabajo, cuánto mostrar las capacidades de nginx de las que a veces se olvidan por completo, considerándolo un servidor proxy banal.


Condiciones


Para evitar que los bots descarguen archivos, se utiliza una prueba "captcha".


Al formar un formulario para un salto de archivo, se crea una imagen con un código y ciertas distorsiones para complicar su reconocimiento automático. También hay almacenamiento para arreglar un par clave + código para verificación.


Tras la confirmación del formulario para descargar el archivo y verificar el código captcha para que coincida con la clave, el usuario recibe un archivo o se genera un enlace único al archivo. La unicidad del enlace está controlada por el backend. El par clave + código también se elimina para evitar su reutilización.


Hay un proxy que redirige todas las solicitudes al backend.


Los problemas


La generación de imágenes complejas es una operación bastante intensiva en recursos, y dado que no se utilizan todos los códigos mostrados. Es necesario crear algún tipo de mecanismo de almacenamiento en caché para las imágenes no utilizadas para poder mostrarlas a otros usuarios.


El código y la clave son verificados por el backend, pero existen dificultades para transferir archivos grandes a través del backend, los enlaces únicos también requieren verificación a nivel de backend, quiero deshacerme de la carga adicional en ellos.


Solución


Seleccionar funcionalidad


En realidad, el captcha en sí consiste en una imagen y una clave determinada que corresponde al código en la imagen que se almacena en el backend. La imagen no es muy grande, la traducimos a Base64 y le damos una forma:


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

O JSON:


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

Si tenemos una parte del formulario que se está formando, entonces podemos usar SSI para insertarlo en el cuerpo de la página, para esto habilitamos el modo correspondiente en la configuración de nginx en el proxy:


 ssi on; 

Y en el código de la página del formulario insertamos:


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

Por lo tanto, hemos asignado la funcionalidad de mapear captcha en ubicaciones o métodos separados. Ahora puedes hacer el almacenamiento en caché.


Sí, el mecanismo de inclusión del lado del servidor (SSI) está casi olvidado, pero el módulo nginx es más vivo que todos los vivos y funciona muy rápidamente. Y, por cierto, si proxy_pass_cache almacena en caché toda la página, el resultado de incluir virtual no se almacenará en caché, sino que se ejecutará cada vez que se solicite. Esto le permite hacer que la inserción sea dinámica.

CAPTCHA CAPTCHA


Para implementar el almacenamiento en caché, necesitamos algo bastante aleatorio y controlado por el número de opciones, la variable $ request_id es adecuada para este rol: es bastante aleatorio y hexadecimal, es decir, al elegir una cierta parte de esta variable, puede limitar el número de elementos de caché a 16 ^ n, donde n - el número de caracteres que necesitamos tomar de la variable. Entonces


Determine la zona de caché:


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

Es importante determinar qué valor de n elegimos, respectivamente, los parámetros dependen de esto:


  • niveles = 1: 2
  • max_size = 128m
  • key_zone = captcha: 10m

Eso fue suficiente para todo, pero no había nada superfluo. A continuación, determinamos la clave de caché:


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

La variable $ captcha_salt sigue siendo útil para nosotros, pero ahora protege contra posibles intersecciones de teclas. Elegí el valor n como 4, lo que significa 16 ^ 4 ranuras de caché, y en promedio se asignan 2kb del tamaño total de caché ( max_size = 128m ) para cada ranura, de lo contrario será necesario aumentar el tamaño máximo.


Hacer la ubicación adecuada


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

Las respuestas "buenas" del backend se almacenarán en caché casi para siempre, el resto no se almacenará en caché. Y sí, puede resaltar de inmediato la funcionalidad de trabajar con captcha en un servicio separado.


Por cierto, se puede usar un mecanismo similar para generar pseudodinámica cuando el usuario presiona F5 y cada vez que se le muestra una nueva "imagen" aleatoria. En este caso, el backend prácticamente no está cargado.

También debemos restablecer el caché correspondiente al verificar el formulario, por lo que el backend, entre otras cosas, debe dar el valor de cache_key para pasarlo de nuevo al formulario como un campo oculto . Desafortunadamente, la directiva proxy_cache_purge solo está disponible en la versión comercial. No importa, hay un módulo cache_purge de terceros, que puede ser un poco más simple, pero suficiente para nosotros. Entonces, ubicación para vaciar el caché:


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

Tiene la directiva interna , ya que no la vamos a usar públicamente. Y para llamar a esta ubicación, usaremos la directiva espejo del módulo http_mirror_module :


Es decir, hacemos una solicitud paralela para restablecer el caché mediante la clave de la variable $ arg_cache_key , que se transmite en el formulario. Luego, simplemente enviamos la solicitud a nuestro backend donde se realiza el resto del procesamiento.


La forma espinosa de optimización


Aquí, de hecho, quería desarrollar un tema: cómo separar la verificación del código captcha y la devolución del archivo. Cómo evitar que el caché se elimine con solicitudes incorrectas. Luego, para optimizar cada vez más, pero todo se reduce al hecho de que, en general, ya no necesitamos el backend ... en absoluto ... porque ya lo tenemos todo.


La tarea que quedó con el servidor en términos de verificación de captcha es verificar la clave + código y eliminar este par del repositorio. Verificar la clave + código puede ser una simple comparación de la cantidad md5 con la clave. Para esto, un módulo es suficiente para nosotros: http_secure_link_module . Es decir, la clave se puede representar como una fórmula:


key = md5_baseurl( salt + code )

Al mismo tiempo, el enlace a la ranura de caché (clave de caché) no nos hará daño, lo agregamos:


key = md5_baseurl( salt + code + cache_key )

Tenemos sal: esta es la variable $ captcha_salt (por lo que fue útil), pero mantener la sal en dos lugares del backend y el proxy es malo, así que hagamos esto:


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

Y deja que el backend vaya al proxy de la sal.


La pregunta permanece con el repositorio, donde almacenamos un par clave + código que necesita ser limpiado. Para hacer esto, el mecanismo de almacenamiento en caché que ya hemos implementado es adecuado para nosotros. Lo único es que no procesamos el resultado cache_purge de ninguna manera , sino que simplemente reflejamos la solicitud, pero esto se puede solucionar. Y sí, eso justifica el uso de una clave de caché al crear una clave captcha.


Verificación de código


Reescribir descargas de archivos de ubicación :


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

Paso los parámetros requeridos con encabezados. Esto es opcional, pero es más conveniente para mí. Proxy el procesamiento a la ubicación local de la verificación captcha. Además, se pasa context = download , de modo que en el controlador podríamos producir uno u otro resultado dependiendo de ello. En este caso, el controlador puede volver a nosotros:


  • 403: error de verificación del código. En realidad, por lo tanto, se incluye proxy_intercept_errors y se declara una ubicación para la redirección en caso de error;
  • 404: error de limpieza de caché. El módulo cache_purge si no hay nada en el caché con dicha clave devuelve 404;
  • 200 + Accel-Redirect : en la ubicación de la carga del archivo, en caso de que el chequeo de captcha haya salido bien. En nuestro caso, será X-Accel-Redirect: / store / file

Si error_page pudiera manejar códigos 2XX , entonces uno podría hacerlo solo. De lo contrario, debe utilizar el mecanismo Accel-Redirect . Si realmente lo desea, puede separar los controladores de errores 403 y 404;

Hacer un error de ubicación simple:


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

Puede devolver cualquier cosa en esta ubicación, según sus necesidades.


Hacemos que la ubicación del archivo se cargue:


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

En primer lugar, es importante que sea interno ; esto significa que no podrá descargar el archivo directamente a través de él, solo a través de la redirección. También se puede modificar en función de las necesidades y no regalar el archivo local, sino que representa la solicitud del servicio de almacenamiento de archivos.


La siguiente ubicación que tenemos para la verificación 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; } 

Tiene 2 bloques: verificación de código y proxy para borrar el caché. Al mismo tiempo, si no se aprobó la verificación del código, devuelva inmediatamente 403 (el texto no es importante, ya que no se usa más).


Proxying a / x / captcha / purge devolverá 2 opciones de respuesta:


  • 200 + Accel-Redirect - tras el lavado exitoso de caché. La redirección será a X-Accel-Redirect: / x / captcha / check / ok ;
  • 404 - si no hubiera nada que limpiar. Este resultado se pasará arriba a / download y se procesará en él error_page ;

Se realiza un controlador separado para la respuesta positiva de / x / captcha / purge debido al hecho de que, en primer lugar, necesitamos alcanzar un mayor nivel de representación, y no entre / download y / x / captcha / check . En segundo lugar, sería bueno dar una respuesta positiva con respecto al contexto.


Comencemos con un controlador de respuesta positiva:


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

En realidad, dependiendo del valor de la variable $ http_x_context ( encabezado X-Context ), podemos determinar qué Accel-Redirect responderá con / x / captcha / check . Esto significa que puede usar este mecanismo en otros lugares además de descargar el archivo.


Borrar el caché es bastante simple:


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

En general, eso es todo, al final obtuvimos la siguiente configuración de 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"; } } 

A qué debe prestar atención:

  • Accel-Redirect solo funciona cuando el estado de respuesta es 2XX. Es cierto, por desgracia, no se ha escrito nada sobre esto en ninguna parte, y los adherentes de nginx no están de acuerdo;
  • Las ubicaciones privadas cercanas permiten 127.0.0.1; negar todo; ya sea interno; , dependiendo de si llegamos a esta ubicación a través de proxy_pass o a través de Accel-Redirect ;
  • Todas las ubicaciones asociadas con captcha se resaltan en / x / capcha / ... para que sea posible formar un microservicio;

Para mayor claridad, también dibujé un diagrama del trabajo:


imagen

Resumen


Como resultado, desde el backend solo necesitamos generar directamente la imagen y el código para ello. Nginx puede manejar fácilmente el resto. Por supuesto, estas son operaciones lógicas relativamente simples, pero sin embargo, esto acelerará significativamente el trabajo y reducirá la carga en el backend. Y, de hecho, no utilizamos ningún mecanismo inusual, sino solo:


  • proxy_cache;
  • Accel-Redirect
  • error_page;
  • enlace_seguro
  • cache_purge;

El resto es la construcción correcta de cadenas lógicas.


También eliminamos el repositorio temporal de backend para códigos y enlaces únicos. Sin embargo, hicieron un elemento obligatorio del sistema nginx, aumentando su peso funcional.

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


All Articles