Und wieder können CAPTCHA oder Nginx auch sticken

Einleitung


Ich ging nach Habr und fand in Entwürfen einen unveröffentlichten Artikel über Captcha. Ich wollte ihn formalisieren und veröffentlichen, beschloss aber, einen neuen Artikel zu schreiben, indem ich den Mechanismus und die verwendeten Werkzeuge leicht änderte. Meiner Meinung nach wird es nützlich sein, einen alten Artikel zu lesen, es wird nicht schlimmer sein.


Das Hauptziel beim Schreiben eines neuen Artikels ist es nicht einmal, einen anderen Arbeitsmechanismus aufzuzeigen, wie sehr man die Fähigkeiten von Nginx zeigt, die manchmal völlig vergessen sind, wenn man es als banalen Proxyserver betrachtet.


Bedingungen


Um zu verhindern, dass Bots Dateien herunterladen, wird ein Test-Captcha verwendet.


Wenn Sie ein Formular für einen Dateisprung erstellen, wird ein Bild mit einem Code und bestimmten Verzerrungen erstellt, um die automatische Erkennung zu erschweren. Es gibt auch Speicherplatz für die Festlegung eines Schlüssel-Code-Paares zur Überprüfung.


Nach Bestätigung des Formulars zum Herunterladen der Datei und Überprüfung des Captcha auf die Entsprechung des Codes wird die Datei dem Benutzer übergeben oder ein eindeutiger einmaliger Link zur Datei erstellt. Die Eindeutigkeit des Links wird auch vom Backend gesteuert. Das Schlüssel-Code-Paar wird ebenfalls entfernt, um seine Wiederverwendung zu verhindern.


Es gibt einen Proxy, der alle Anfragen an das Backend weiterleitet.


Die Probleme


Die komplexe Bilderzeugung ist eine ziemlich ressourcenintensive Operation, und da nicht alle gezeigten Codes verwendet werden. Es muss eine Art Caching-Mechanismus erstellt werden, damit nicht verwendete Bilder anderen Benutzern angezeigt werden können.


Der Code und der Schlüssel werden vom Backend überprüft, aber es gibt Schwierigkeiten beim Übertragen großer Dateien über das Backend. Einmalige Verknüpfungen müssen auch auf Backend-Ebene überprüft werden. Ich möchte die zusätzliche Last auf ihnen loswerden.


Lösung


Wählen Sie die Funktionalität


Tatsächlich besteht das Captcha selbst aus einem Bild und einem bestimmten Schlüssel, der dem Code im Bild entspricht, der im Backend gespeichert ist. Das Bild ist nicht sehr groß, und wir übersetzen es in Base64 und geben entweder ein Stück Form:


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

Oder JSON:


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

Wenn ein Teil des Formulars erstellt wird, können wir es mit SSI in den Hauptteil der Seite einfügen. Dazu aktivieren wir den entsprechenden Modus in der Nginx-Konfiguration auf dem Proxy:


 ssi on; 

Und in den Code der Formularseite fügen wir ein:


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

Aus diesem Grund haben wir die Funktionen zum Zuordnen von Captcha in separate Positionen oder Methoden aufgeteilt. Jetzt können Sie das Caching durchführen.


Ja, der Server Side Include (SSI) -Mechanismus ist fast vergessen, aber das Nginx-Modul ist lebendiger als alle lebenden und funktioniert sehr schnell. Übrigens: Wenn proxy_pass_cache die gesamte Seite zwischenspeichert, wird das Ergebnis von include virtual nicht zwischengespeichert, sondern bei jeder Anforderung ausgeführt. Auf diese Weise können Sie die Einfügung dynamisch gestalten.

CAPTCHA CAPTCHA


Um das Cachen zu implementieren, benötigen wir etwas ziemlich Zufälliges, das durch die Anzahl der Optionen gesteuert wird. Die Variable $ request_id ist für diese Rolle geeignet. Sie ist ziemlich zufällig und hexadezimal, dh Sie können durch Auswahl eines bestimmten Teils dieser Variablen die Anzahl der Cache-Elemente auf 16 ^ n begrenzen n - die Anzahl der Zeichen, die wir von der Variablen nehmen müssen. Also:


Bestimmen Sie die Cache-Zone:


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

Es ist wichtig zu bestimmen, welchen Wert von n wir wählen bzw. welche Parameter davon abhängen:


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

Das war genug für alles, aber nichts war überflüssig. Als nächstes bestimmen wir den Cache-Schlüssel:


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

Die Variable $ captcha_salt ist für uns immer noch nützlich, schützt aber jetzt vor möglichen Schlüsselüberschneidungen. Ich habe n als 4 gewählt, was 16 ^ 4 Cache-Slots bedeutet, wobei jedem Slot durchschnittlich 2 KB aus der Gesamt-Cache-Größe ( max_size = 128 m ) zugewiesen werden. Dies sollte ausreichen, da Sie sonst die maximale Größe erhöhen müssen.


Den richtigen Ort finden


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

Die "guten" Backend-Antworten werden fast für immer zwischengespeichert, der Rest wird nicht zwischengespeichert. Und ja, Sie können die Funktionalität der Arbeit mit Captcha sofort in einem separaten Dienst hervorheben.


Übrigens kann ein ähnlicher Mechanismus verwendet werden, um eine Pseudodynamik zu bilden, wenn der Benutzer F5 drückt und jedes Mal, wenn ihm ein neues zufälliges "Bild" gezeigt wird. In diesem Fall ist das Backend praktisch nicht geladen.

Wir müssen auch den entsprechenden Cache zurücksetzen, wenn wir das Formular überprüfen. Daher muss das Backend unter anderem den Wert cache_key angeben , damit er als verstecktes Feld an das Formular zurückgegeben wird. Leider ist die proxy_cache_purge- Direktive nur in der kommerziellen Version verfügbar. Es spielt keine Rolle, dass es ein cache_purge- Modul eines Drittanbieters gibt , das zwar etwas einfacher ist, aber für uns ausreicht. Speicherort für das Leeren des Caches:


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

Es hat die interne Richtlinie, da wir sie nicht öffentlich verwenden werden. Um diesen Speicherort aufzurufen , verwenden wir die mirror- Direktive des http_mirror_module- Moduls:


Das heißt, wir fordern parallel dazu an, den Cache mit dem Schlüssel der Variablen $ arg_cache_key zurückzusetzen , der im Formular übertragen wird. Als nächstes haben wir die Anfrage an unser Backend weitergeleitet, wo der Rest der Verarbeitung durchgeführt wird.


Der dornige Weg der Optimierung


Hier wollte ich eigentlich ein Thema entwickeln: Wie trenne ich die Überprüfung des Captcha-Codes und die Rückgabe der Datei? Verhindert, dass der Cache bei falschen Anforderungen gelöscht wird. Dann immer mehr zu optimieren, aber es kommt darauf an, dass wir das Backend im Allgemeinen nicht mehr brauchen ... überhaupt ... weil wir schon alles haben.


Die Aufgabe, die bei der Captcha-Überprüfung auf dem Server verbleibt, besteht darin, den Schlüssel + Code zu überprüfen und dieses Paar aus dem Repository zu entfernen. Das Überprüfen des Schlüssels + Codes kann ein einfacher Vergleich der md5-Menge mit dem Schlüssel sein. Dafür reicht uns ein Modul: http_secure_link_module . Das heißt, der Schlüssel kann als Formel dargestellt werden:


key = md5_baseurl( salt + code )

Gleichzeitig schadet das Binden an den Cache-Slot (Cache-Schlüssel) nicht, wir fügen es hinzu:


key = md5_baseurl( salt + code + cache_key )

Wir haben salt - das ist die Variable $ captcha_salt (so hat es sich als nützlich erwiesen ), aber das salt an zwei Stellen des Backends und des Proxys aufzubewahren ist schlecht, also lasst uns das machen:


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

Und lassen Sie das Backend zum Proxy für das Salz gehen.


Die Frage bleibt beim Speicher, in dem wir ein Schlüssel-Code-Paar speichern, das bereinigt werden muss. Hierzu eignet sich für uns der bereits implementierte Caching-Mechanismus. Das einzige, was wir tun, ist, dass wir das cache_purge- Ergebnis in keiner Weise verarbeiten , sondern einfach die Anfrage dafür spiegeln, aber das ist korrigierbar. Und ja, das rechtfertigt die Verwendung eines Cache-Schlüssels beim Erstellen eines Captcha-Schlüssels.


Code überprüfen


Rewrite Location File Downloads:


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

Ich übergebe die erforderlichen Parameter mit Überschriften. Dies ist optional, aber für mich bequemer. Wir übermitteln die Verarbeitung an den lokalen Speicherort der Captcha-Prüfung. Zusätzlich wird context = download übergeben, so dass wir im Handler abhängig davon das eine oder andere Ergebnis erzeugen können. In diesem Fall kann der Handler entweder zu uns zurückkehren:


  • 403 - Codeüberprüfungsfehler. Daher wird proxy_intercept_errors eingeschlossen und ein Speicherort für die Umleitung im Falle eines Fehlers deklariert.
  • 404 - Cache-Bereinigungsfehler. Das cache_purge- Modul gibt 404 zurück, wenn sich mit einem solchen Schlüssel nichts im Cache befindet.
  • 200 + Accel-Redirect - am Speicherort des Datei-Uploads, falls die Captcha-Prüfung erfolgreich war. In unserem Fall ist dies X-Accel-Redirect: / store / file

Wenn error_page 2XX- Codes verarbeiten könnte, könnte man das alleine tun. Andernfalls müssen Sie den Accel-Redirect- Mechanismus verwenden. Wenn Sie wirklich möchten, können Sie die Fehlerbehandlungsroutinen 403 und 404 trennen.

Einen einfachen Standortfehler machen :


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

Sie können alles an diesem Ort zurückgeben, je nach Ihren Bedürfnissen.


Wir machen den Ort des Dateiuploads:


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

Erstens ist es wichtig, dass es sich um eine interne Datei handelt. Dies bedeutet, dass Sie die Datei nur durch Umleitung direkt herunterladen können. Es kann auch je nach Bedarf geändert werden und die lokale Datei nicht weitergeben, sondern die Anforderung für den Dateispeicherdienst per Proxy weiterleiten.


Den folgenden Speicherort haben wir für die Captcha-Überprüfung:


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

Es hat 2 Blöcke: Codeüberprüfung und Proxy-Zugriff, um den Cache zu leeren. Wenn die Codeüberprüfung nicht bestanden wurde, wird sofort 403 zurückgegeben (der Text ist unwichtig, da er nicht weiter verwendet wird).


Wenn Sie nach / x / captcha / purge gehen, werden 2 Antwortoptionen zurückgegeben:


  • 200 + Accel-Redirect - bei erfolgreicher Cache-Leerung. Die Weiterleitung erfolgt zu X-Accel-Redirect: / x / captcha / check / ok ;
  • 404 - wenn es nichts zu reinigen gäbe. Dieses Ergebnis wird oben an / download übergeben und dort verarbeitet. Error_page ;

Ein separater Handler für die positive Antwort von / x / captcha / purge wird erstellt, da zunächst eine höhere Proxy-Ebene erreicht werden muss und nicht zwischen / download und / x / captcha / check . Zweitens wäre es schön, eine positive Antwort zum Kontext zu geben.


Beginnen wir mit einem positiven Response-Handler:


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

Tatsächlich können wir abhängig vom Wert der Variablen $ http_x_context ( X-Context- Header) bestimmen, welche Accel-Redirect mit / x / captcha / check antwortet. Dies bedeutet, dass Sie diesen Mechanismus nicht nur zum Herunterladen der Datei verwenden können.


Das Leeren des Caches ist ganz einfach:


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

Im Allgemeinen ist das alles, am Ende haben wir die folgende Nginx-Konfiguration:


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

Was Sie beachten sollten:

  • Accel-Redirect funktioniert nur, wenn der Antwortstatus 2XX ist. Leider ist nirgendwo etwas darüber geschrieben worden, und Nginx-Anhänger sind anderer Meinung.
  • Private Standorte in der Nähe erlauben entweder 127.0.0.1; leugne alles; entweder intern; Je nachdem, ob wir über proxy_pass oder über Accel-Redirect an diesen Speicherort gelangen.
  • Alle mit dem Captcha verknüpften Stellen werden in / x / capcha / ... hervorgehoben, damit ein Microservice erstellt werden kann.

Zur Verdeutlichung habe ich auch ein Diagramm der Arbeit gezeichnet:


Bild

Zusammenfassung


Aus diesem Grund müssen wir vom Backend aus nur das Bild und den Code dafür direkt generieren. Den Rest kann Nginx problemlos erledigen. Natürlich handelt es sich hierbei um relativ einfache logische Operationen, dies wird jedoch die Arbeit erheblich beschleunigen und die Belastung des Backends verringern. Tatsächlich verwendeten wir keine ungewöhnlichen Mechanismen, sondern nur:


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

Der Rest ist der korrekte Aufbau logischer Ketten.


Wir haben auch das temporäre Backend-Repository für Codes und einmalige Links entfernt. Sie machten jedoch ein obligatorisches Element des Nginx-Systems und erhöhten sein Funktionsgewicht.

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


All Articles