Pendahuluan
Saya pergi ke Habr dan menemukan dalam draf sebuah artikel yang tidak dipublikasikan tentang captcha, saya ingin meresmikan dan menerbitkannya, tetapi memutuskan untuk menulis artikel baru dengan sedikit mengubah mekanisme dan alat yang digunakan. Menurut pendapat saya itu akan berguna untuk membaca artikel lama, itu tidak akan lebih buruk.
Tujuan utama dari penulisan artikel baru adalah bukan untuk menunjukkan mekanisme kerja lain, seberapa banyak untuk menunjukkan kemampuan nginx yang kadang-kadang benar-benar dilupakan, menganggapnya sebagai server proxy dangkal.
Ketentuan
Untuk mencegah bot mengunduh file, tes "captcha" digunakan.
Saat membentuk formulir untuk lompatan file, gambar dengan kode dan distorsi tertentu dibuat untuk mempersulit pengenalan otomatisnya. Ada juga penyimpanan untuk memperbaiki pasangan kode + kunci untuk verifikasi.
Setelah konfirmasi formulir untuk mengunduh file dan memeriksa captcha untuk korespondensi kode, pengguna diberikan file atau tautan satu kali unik ke file yang dihasilkan. Keunikan tautan juga dikendalikan oleh backend. Pasangan kunci + kode juga dihapus untuk mencegah penggunaannya kembali.
Ada proksi yang mengalihkan semua permintaan ke backend.
Masalahnya
Pembuatan gambar yang kompleks adalah operasi yang membutuhkan banyak sumber daya, dan mengingat bahwa tidak semua kode yang ditampilkan digunakan. Hal ini diperlukan untuk membuat mekanisme tertentu untuk melakukan cache gambar yang tidak digunakan agar dapat menampilkannya kepada pengguna lain.
Kode dan kunci diperiksa oleh backend, tetapi ada kesulitan dengan mentransfer file besar melalui backend, satu kali tautan juga memerlukan pemeriksaan di tingkat backend, saya ingin menyingkirkan beban tambahan pada mereka.
Solusi
Pilih fungsionalitas
Sebenarnya, captcha itu sendiri terdiri dari gambar dan kunci tertentu yang sesuai dengan kode pada gambar yang disimpan di backend. Gambar tidak terlalu besar, dan kami menerjemahkannya ke Base64 dan memberikan salah satu bentuk:
<img src="data:image/png;base64,{{ IMAGE CODE BASE64 }}"> <input type="hidden" name="key" value="{{ KEY }}">
Atau JSON:
{ ... "data": { "image": "data:image/png;base64,{{ IMAGE CODE BASE64 }}", "key": "{{ KEY }}" } }
Jika kita memiliki sepotong formulir yang sedang dibentuk, maka kita dapat menggunakan SSI untuk memasukkannya ke badan halaman, untuk ini kita mengaktifkan mode yang sesuai dalam konfigurasi nginx pada proxy:
ssi on;
Dan dalam kode halaman formulir kami masukkan:
... <form action="download" method="get" ...> ... ... </form> ...
Dengan demikian, kami telah mengalokasikan fungsi pemetaan captcha ke lokasi atau metode yang terpisah. Sekarang Anda bisa melakukan caching.
Ya, mekanisme Server Side Include (SSI) hampir terlupakan, tetapi modul nginx untuknya lebih hidup daripada semua yang hidup dan bekerja dengan sangat cepat. Dan omong-omong, jika proxy_pass_cache cache seluruh halaman, hasil dari virtual tidak akan di-cache, tetapi akan dieksekusi setiap kali diminta. Ini memungkinkan Anda untuk membuat sisipan menjadi dinamis.
CAPTCHA CAPTCHA
Untuk mengimplementasikan caching, kita perlu sesuatu yang cukup acak dan dikendalikan oleh jumlah opsi, variabel $ request_id cocok untuk peran ini - itu cukup acak dan heksadesimal, yaitu, dengan memilih bagian tertentu dari variabel ini, Anda dapat membatasi jumlah elemen cache hingga 16 ^ n, di mana n - jumlah karakter yang perlu kita ambil dari variabel. Jadi:
Tentukan zona cache:
proxy_cache_path /cache/nginx/captcha levels=1:1 keys_zone=captcha:10m max_size=128m;
Penting untuk menentukan nilai n yang kita pilih, masing-masing, parameternya bergantung pada ini:
- level = 1: 2
- max_size = 128m
- keys_zone = captcha: 10m
Jadi itu sudah cukup untuk semuanya, tetapi tidak ada yang berlebihan. Selanjutnya, kami menentukan kunci cache:
server { ... set $captcha_salt 'salt'; if ( $request_id ~* "(\w{4})$" ) { set $cache_key $1; } ...
Variabel $ captcha_salt masih berguna bagi kami, tetapi sekarang ia melindungi terhadap kemungkinan persimpangan kunci. Saya memilih n sebagai 4, yang berarti 16 ^ 4 slot cache, dengan rata-rata 2kb dialokasikan untuk setiap slot dari total ukuran cache ( max_size = 128m ), yang seharusnya cukup, jika tidak, Anda perlu menaikkan ukuran maksimum.
Membuat lokasi yang sesuai
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; }
Respons backend "baik" akan di-cache hampir selamanya, sisanya tidak akan di-cache. Dan ya, Anda dapat langsung menyoroti fungsi bekerja dengan captcha di layanan terpisah.
Omong-omong, mekanisme serupa dapat digunakan untuk menghasilkan dinamika semu ketika pengguna menekan F5 dan setiap kali "gambar" acak baru ditampilkan kepadanya. Dalam hal ini, backend praktis tidak dimuat.
Kita juga perlu mengatur ulang cache yang sesuai saat memeriksa formulir, jadi backend, antara lain, harus memberikan nilai cache_key untuk meneruskannya kembali ke formulir sebagai bidang tersembunyi . Sayangnya, direktif proxy_cache_purge hanya tersedia dalam versi komersial. Tidak masalah, ada modul cache_purge pihak ketiga , yang mungkin sedikit lebih sederhana, tetapi cukup bagi kita. Jadi, lokasi pembilasan cache:
location /x/captcha/cache/purge { internal; proxy_cache_purge captcha "$captcha_salt:$arg_cache_key"; }
Ini memiliki arahan internal , karena kita tidak akan menggunakannya untuk umum. Dan untuk memanggil lokasi ini , kita akan menggunakan arahan cermin dari modul http_mirror_module :
Artinya, kami membuat permintaan paralel untuk mereset cache dengan kunci variabel $ arg_cache_key , yang dikirimkan dalam formulir. Selanjutnya, cukup proksi permintaan ke backend kami di mana sisa pemrosesan dilakukan.
Cara Optimasi Berduri
Di sini, sebenarnya, saya ingin mengembangkan sebuah topik: bagaimana memisahkan verifikasi kode captcha dan pengembalian file. Cara mencegah cache agar tidak dicuci dengan permintaan yang salah. Kemudian untuk mengoptimalkan lebih banyak dan lebih banyak, tetapi semuanya bermuara pada kenyataan bahwa secara umum kita tidak lagi membutuhkan backend ... sama sekali ... karena kita sudah memiliki segalanya.
Tugas yang tetap dengan server dalam hal verifikasi captcha sebenarnya memeriksa kode kunci + dan menghapus pasangan ini dari repositori. Memeriksa kode kunci + dapat berupa perbandingan sederhana dari jumlah md5 dengan kunci. Untuk ini, sebuah modul sudah cukup bagi kami: http_secure_link_module . Artinya, kuncinya dapat direpresentasikan sebagai rumus:
key = md5_baseurl( salt + code )
Pada saat yang sama, mengikat slot cache (kunci cache) tidak akan merugikan kami, kami menambahkannya:
key = md5_baseurl( salt + code + cache_key )
Kami memiliki garam - ini adalah variabel $ captcha_salt (jadi berguna), tetapi menjaga garam di dua tempat backend dan proxy buruk, jadi mari kita lakukan ini:
location /x/captcha/salt { allow {{ captcha backend IPs }}; deny all; return 200 "$captcha_salt"; }
Dan biarkan backend pergi ke proxy untuk mendapatkan garam.
Pertanyaannya tetap pada repositori, tempat kami menyimpan pasangan kunci + kode yang perlu dibersihkan. Untuk melakukan ini, mekanisme caching yang telah kami implementasikan cocok untuk kami. Satu-satunya hal adalah bahwa kita tidak memproses hasil cache_purge dengan cara apa pun , tetapi hanya mencerminkan permintaan untuk itu, tetapi ini dapat diperbaiki. Dan ya, itu dibenarkan menggunakan kunci cache saat membuat kunci captcha.
Pemeriksaan kode
Tulis ulang unduhan file lokasi :
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; }
Saya melewati parameter yang diperlukan dengan header. Ini opsional, tetapi lebih nyaman bagi saya. Kami proksi pemrosesan ke lokasi lokal dari cek captcha. Selain itu, konteks = unduhan dilewati, sehingga dalam handler kita dapat menghasilkan satu atau hasil lain bergantung padanya. Dalam hal ini, pawang dapat kembali kepada kami:
- 403 - kesalahan verifikasi kode. Sebenarnya, oleh karena itu, proxy_intercept_errors diaktifkan dan lokasi dideklarasikan untuk pengalihan jika terjadi kesalahan;
- 404 - kesalahan pembersihan cache. Modul cache_purge jika tidak ada dalam cache dengan kunci yang mengembalikan 404;
- 200 + Accel-Redirect - di lokasi unggahan file, kalau-kalau cek captcha baik-baik saja. Dalam kasus kami, ini akan menjadi X-Accel-Redirect: / store / file
Jika error_page dapat menangani kode 2XX , maka orang dapat melakukannya sendiri. Kalau tidak, Anda harus menggunakan mekanisme Accel-Redirect . Jika Anda benar-benar ingin, Anda dapat memisahkan penangan kesalahan 403 dan 404;
Membuat kesalahan lokasi sederhana:
location /download/fail { internal; return 200 "FAIL DOWNLOAD"; }
Anda dapat mengembalikan apa pun di lokasi ini, tergantung pada kebutuhan Anda.
Kami membuat lokasi unggahan file:
location /store/file { internal; add_header Content-Disposition "attachment; filename=\"$arg_filename\""; alias /spool/tmp/; try_files $arg_filename =404; }
Pertama, penting bahwa itu internal - ini berarti Anda tidak akan dapat mengunduh file langsung melalui itu, hanya melalui pengalihan. Itu juga dapat dimodifikasi tergantung pada kebutuhan dan tidak memberikan file lokal, tetapi untuk proksi permintaan untuk layanan penyimpanan file.
Lokasi berikut yang kami miliki untuk verifikasi 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; }
Ini memiliki 2 blok: verifikasi kode dan proksi lebih lanjut untuk menghapus cache. Pada saat yang sama, jika pemeriksaan kode tidak lulus, maka segera kembali 403 (teks tidak penting, karena tidak digunakan lebih lanjut).
Proxy ke / x / captcha / purge akan mengembalikan 2 pilihan jawaban:
- 200 + Accel-Redirect - setelah pembilasan cache berhasil. Redirect akan ke X-Accel-Redirect: / x / captcha / check / ok ;
- 404 - jika tidak ada yang dibersihkan. Hasil ini akan diteruskan ke / unduh di atas dan akan diproses di dalamnya error_page ;
Penangan terpisah untuk respons positif dari / x / captcha / purge dibuat karena fakta bahwa pertama-tama kita perlu mencapai tingkat proxy yang lebih tinggi, dan bukan antara / unduh dan / x / captcha / check . Kedua, akan menyenangkan untuk memberikan jawaban positif Anda mengenai konteksnya.
Mari kita mulai dengan penangan respons positif:
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"; }
Sebenarnya, tergantung pada nilai variabel $ http_x_context (header X-Context ), kita dapat menentukan Accel-Redirect yang akan merespons dengan / x / captcha / check . Ini berarti Anda dapat menggunakan mekanisme ini di tempat lain selain mengunduh file.
Menghapus cache cukup sederhana:
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"; }
Secara umum, itu saja, pada akhirnya kami mendapat konfigurasi nginx berikut:
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"; } }
Apa yang harus Anda perhatikan:
- Accel-Redirect hanya berfungsi ketika status responsnya 2XX. Benar, sayangnya, tidak ada yang ditulis tentang ini di mana pun, dan penganut nginx tidak setuju;
- Lokasi pribadi tutup memungkinkan 127.0.0.1; tolak semua; baik internal; , tergantung pada apakah kita sampai ke lokasi ini melalui proxy_pass , atau melalui Accel-Redirect ;
- Semua lokasi yang terkait dengan captcha disorot di / x / capcha / ... sehingga dimungkinkan untuk membentuk layanan mikro;
Untuk lebih jelasnya, saya juga menggambar diagram karya:

Ringkasan
Akibatnya, dari backend kita hanya perlu secara langsung menghasilkan gambar dan kode untuk itu. Nginx dapat dengan mudah menangani sisanya. Tentu saja, ini adalah operasi logis yang relatif sederhana, namun demikian, ini akan secara signifikan mempercepat pekerjaan dan mengurangi beban di backend. Dan faktanya, kami tidak menggunakan mekanisme yang tidak biasa, tetapi hanya:
- proxy_cache;
- Pengalihan-Accel
- error_page;
- secure_link
- cache_purge;
Sisanya adalah konstruksi rantai logis yang benar.
Kami juga menghapus repositori backend sementara untuk kode dan tautan satu kali. Namun, mereka membuat elemen wajib dari sistem nginx, meningkatkan bobot fungsionalnya.