Et encore une fois, CAPTCHA ou nginx sait aussi broder

Présentation


Je suis allé ici à Habr et j'ai trouvé dans les brouillons un article non publié sur le captcha, je voulais le formaliser et le publier, mais j'ai décidé d'écrire un nouvel article en changeant légèrement le mécanisme et les outils utilisés. À mon avis, il sera utile de lire un vieil article, ce ne sera pas pire.


L'objectif principal de la rédaction d'un nouvel article n'est même pas de montrer un autre mécanisme de travail, combien montrer les capacités de nginx dont ils oublient parfois complètement, le considérant comme un serveur proxy banal.


Les conditions


Pour empêcher les bots de télécharger des fichiers, un test «captcha» est utilisé.


Lors de la formation d'un formulaire pour un saut de fichier, une image avec un code et certaines distorsions est créée pour compliquer sa reconnaissance automatique. Il y a également un stockage pour fixer une paire clé + code pour vérification.


Lors de la confirmation du formulaire de téléchargement du fichier et de vérification du captcha pour la correspondance du code, le fichier est remis à l'utilisateur ou un lien unique unique vers le fichier est généré. L'unicité du lien est également contrôlée par le backend. La paire clé + code est également supprimée pour empêcher sa réutilisation.


Il existe un proxy qui redirige toutes les demandes vers le backend.


Les problèmes


La génération d'images complexes est une opération assez gourmande en ressources, et étant donné que tous les codes affichés ne sont pas utilisés. Il est nécessaire de créer un certain mécanisme pour mettre en cache les images inutilisées afin de pouvoir les afficher aux autres utilisateurs.


Le code et la clé sont vérifiés par le backend, mais il y a des difficultés à transférer de gros fichiers via le backend, les liens uniques nécessitent également une vérification au niveau du backend, je veux me débarrasser de la charge supplémentaire sur eux.


Solution


Sélectionnez la fonctionnalité


En fait, le captcha lui-même se compose d'une image et d'une certaine clé qui correspond au code dans l'image qui est stockée sur le backend. L'image n'est pas très grande, et nous la traduisons en Base64 et donnons soit un morceau de forme:


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

Si nous avons un morceau du formulaire en cours de formation, nous pouvons utiliser SSI pour l'insérer dans le corps de la page, pour cela, nous activons le mode correspondant dans la configuration nginx sur le proxy:


 ssi on; 

Et dans le code de la page du formulaire, nous insérons:


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

Ainsi, nous avons attribué la fonctionnalité de mappage du captcha dans un emplacement ou des méthodes séparés. Vous pouvez maintenant effectuer la mise en cache.


Oui, le mécanisme Server Side Include (SSI) est presque oublié, mais le module nginx pour lui est plus vivant que tous les vivants et fonctionne très rapidement. Et au fait, si proxy_pass_cache met en cache la page entière, le résultat de include virtual ne sera pas mis en cache, mais sera exécuté chaque fois qu'il sera demandé. Cela vous permet de rendre l'insertion dynamique.

CAPTCHA CAPTCHA


Pour implémenter la mise en cache, nous avons besoin de quelque chose d'assez aléatoire et contrôlé par le nombre d'options, la variable $ request_id convient à ce rôle - elle est assez aléatoire et hexadécimale, c'est-à-dire qu'en choisissant une certaine partie de cette variable, vous pouvez limiter le nombre d'éléments de cache à 16 ^ n, où n - le nombre de caractères que nous devons prendre dans la variable. Donc:


Déterminez la zone de cache:


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

Il est important de déterminer quelle valeur de n nous choisissons, respectivement, les paramètres en dépendent:


  • niveaux = 1: 2
  • max_size = 128m
  • keys_zone = captcha: 10m

C'était donc suffisant pour tout, mais il n'y avait rien de superflu. Ensuite, nous déterminons la clé de cache:


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

La variable $ captcha_salt nous est toujours utile, mais maintenant elle protège contre les intersections de clés possibles. J'ai choisi n comme 4, ce qui signifie 16 ^ 4 emplacements de cache, avec 2 Ko en moyenne alloués à chaque emplacement de la taille totale du cache ( max_size = 128m ), ce qui devrait être suffisant, sinon vous devez augmenter la taille maximale.


Faire l' emplacement approprié


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

Les «bonnes» réponses backend seront mises en cache presque pour toujours, le reste ne sera pas mis en cache. Et oui, vous pouvez immédiatement mettre en évidence la fonctionnalité de travailler avec captcha dans un service distinct.


Soit dit en passant, un mécanisme similaire peut être utilisé pour générer une pseudo-dynamique lorsque l'utilisateur appuie sur F5 et chaque fois qu'une nouvelle «image» aléatoire lui est montrée. Dans ce cas, le backend n'est pratiquement pas chargé.

Nous devons également réinitialiser le cache correspondant lors de la vérification du formulaire, de sorte que le backend, entre autres, doit donner la valeur cache_key pour le renvoyer au formulaire en tant que champ masqué . Malheureusement, la directive proxy_cache_purge n'est disponible que dans la version commerciale. Peu importe, il existe un module cache_purge tiers, qui peut être légèrement plus simple, mais suffisant pour nous. Donc, emplacement pour vider le cache:


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

Il a la directive interne , car nous n'allons pas l'utiliser publiquement. Et pour appeler cet emplacement, nous utiliserons la directive miroir du module http_mirror_module :


Autrement dit, nous faisons une demande parallèle pour réinitialiser le cache par la clé de la variable $ arg_cache_key , qui est transmise sous la forme. Ensuite, il suffit de transmettre la requête par proxy à notre backend où le reste du traitement est effectué.


La voie épineuse de l'optimisation


Ici, en fait, je voulais développer un sujet: comment séparer la vérification du code captcha et le retour du fichier. Comment empêcher le cache d'être effacé avec des requêtes incorrectes. Ensuite pour optimiser de plus en plus, mais tout se résume au fait qu'en général on n'a plus besoin du backend ... du tout ... car on a déjà tout.


La tâche qui restait avec le serveur en termes de vérification du captcha était en fait de vérifier la clé + le code et de supprimer cette paire du référentiel. La vérification de la clé + du code peut être une simple comparaison du montant md5 avec la clé. Pour cela, un module nous suffit: http_secure_link_module . Autrement dit, la clé peut être représentée sous la forme d'une formule:


key = md5_baseurl( salt + code )

Dans le même temps, la liaison à l'emplacement de cache (clé de cache) ne nous fera pas de mal, nous l'ajoutons:


key = md5_baseurl( salt + code + cache_key )

Nous avons Salt - c'est la variable $ captcha_salt (donc c'est utile), mais garder le sel à deux endroits sur le backend et le proxy est mauvais, alors faisons ceci:


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

Et laissez le backend aller au proxy pour le sel.


La question demeure avec le référentiel, où nous stockons une paire clé + code qui doit être nettoyée. Pour ce faire, le mécanisme de mise en cache que nous avons déjà implémenté nous convient. La seule chose est que nous ne traitons pas le résultat cache_purge de quelque façon que ce soit , mais simplement en miroir de la demande, mais cela est réparable. Et oui, cela justifie l'utilisation d'une clé de cache lors de la création d'une clé captcha.


Vérification du code


Réécrire les téléchargements de fichiers d' emplacement :


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

Je passe les paramètres requis avec des en-têtes. C'est facultatif, mais c'est plus pratique pour moi. Nous procurons le traitement à l'emplacement local du chèque captcha. De plus, context = download est passé, de sorte que dans le gestionnaire, nous pourrions produire l'un ou l'autre résultat en fonction de celui-ci. Dans ce cas, le gestionnaire peut nous retourner soit:


  • 403 - erreur de vérification de code. En fait, par conséquent, proxy_intercept_errors est activé et l'emplacement est déclaré pour la redirection en cas d'erreur;
  • 404 - erreur de nettoyage du cache. Le module cache_purge s'il n'y a rien dans le cache avec une telle clé retourne 404;
  • 200 + Accel-Redirect - sur l' emplacement du téléchargement du fichier, au cas où la vérification du captcha se serait bien passée. Dans notre cas, ce sera X-Accel-Redirect: / store / file

Si error_page pouvait gérer les codes 2XX , alors on pouvait le faire seul. Sinon, vous devez utiliser le mécanisme Accel-Redirect . Si vous le souhaitez, vous pouvez séparer les gestionnaires d'erreurs 403 et 404;

Faire une simple erreur de localisation :


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

Vous pouvez retourner n'importe quoi à cet endroit, selon vos besoins.


Nous faisons l'emplacement du téléchargement du fichier:


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

Tout d'abord, il est important qu'il soit interne - cela signifie que vous ne pourrez pas télécharger le fichier directement via celui-ci, uniquement par redirection. Il peut également être modifié en fonction des besoins et ne pas donner le fichier local, mais pour proxy la demande pour le service de stockage de fichiers.


L' emplacement suivant que nous avons pour la vérification du 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; } 

Il a 2 blocs: vérification du code et procuration supplémentaire pour vider le cache. De plus, si la vérification du code n'a pas réussi, renvoyez immédiatement 403 (le texte est sans importance, car il n'est plus utilisé).


La procuration à / x / captcha / purge renverra 2 options de réponse:


  • 200 + Accel-Redirect - lors du vidage réussi du cache. La redirection sera vers X-Accel-Redirect: / x / captcha / check / ok ;
  • 404 - s'il n'y avait rien à nettoyer. Ce résultat sera transmis ci-dessus à / download et y sera traité error_page ;

Un gestionnaire distinct pour la réponse positive de / x / captcha / purge est créé car nous devons d'abord atteindre un niveau de proxy plus élevé, et non entre / download et / x / captcha / check . Deuxièmement, il serait bon de donner votre réponse positive concernant le contexte.


Commençons par un gestionnaire de réponse positive:


 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 fait, en fonction de la valeur de la variable $ http_x_context (en - tête X-Context ), nous pouvons déterminer quelle Accel-Redirect va répondre avec / x / captcha / check . Cela signifie que vous pouvez utiliser ce mécanisme à d'autres endroits que le téléchargement du fichier.


Vider le cache est assez 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 général, c'est tout, à la fin, nous avons obtenu la configuration nginx suivante:


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

À quoi devez-vous faire attention:

  • Accel-Redirect ne fonctionne que lorsque l'état de la réponse est 2XX. Certes, hélas, rien n'a été écrit à ce sujet nulle part, et les adhérents de nginx ne sont pas d'accord;
  • Les emplacements privés proches permettent soit 127.0.0.1; nier tout; soit interne; , selon que nous arrivons à cet emplacement via proxy_pass ou via Accel-Redirect ;
  • Tous les emplacements associés au captcha sont mis en évidence dans / x / capcha / ... de sorte qu'il serait possible de former un microservice;

Pour plus de clarté, j'ai également dessiné un schéma du travail:


image

Résumé


Par conséquent, à partir du backend, nous n'avons qu'à générer directement l'image et le code pour celle-ci. Nginx peut facilement gérer le reste. Bien sûr, ce sont des opérations logiques relativement simples, mais néanmoins, cela accélérera considérablement le travail et réduira la charge sur le backend. Et en fait, nous n'avons utilisé aucun mécanisme inhabituel, mais seulement:


  • proxy_cache;
  • Accel-Redirect
  • page_erreur;
  • secure_link
  • cache_purge;

Le reste est la construction correcte de chaînes logiques.


Nous avons également supprimé le référentiel backend temporaire pour les codes et les liens à usage unique. Cependant, ils ont fait un élément obligatoire du système nginx, augmentant son poids fonctionnel.

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


All Articles