Cara Memecahkan CAPTCHA di Job Queue Laravel (SDK PHP)

laravel queue captcha - How to Solve CAPTCHAs in a Laravel Queue Job (PHP SDK)

Untuk memecahkan CAPTCHA di job queue Laravel, panggil klien PHP CapSkip di dalam metode handle milik job dan kirim token dari job yang sama. Pemanggilannya singkat. Yang perlu diperhatikan adalah tiga angka bawaan Laravel yang dirancang untuk job yang selesai dalam hitungan detik: worker memberi setiap job 60 detik, queue menyerahkan job ke worker lain setelah 90 detik, dan setiap job mendapat satu percobaan. Pemecahan reCAPTCHA bisa melakukan polling hingga 300 detik, jadi dengan nilai default, job captcha queue Laravel akan dimatikan di tengah jalan atau ditandai gagal padahal masih memecahkan, dan begitu Anda menambahkan retry, job itu bisa berjalan dua kali. Panduan ini membahas job-nya, timeout, aturan retry, dan satu hal yang berubah ketika worker berjalan di Windows.

Apa yang Anda butuhkan

  • Laravel 11 atau lebih baru. Job ini memakai trait Queueable tunggal yang diperkenalkan di versi 11; semua hal lain di sini juga berfungsi di versi 10. Laravel 13 juga bisa menyatakan pengaturan job sebagai atribut seperti #[Tries(3)], dan properti biasa yang dipakai di bawah tetap berfungsi di sana.
  • Paket PHP CapSkip, yang membutuhkan PHP 8.0 atau lebih baru dengan ekstensi curl dan json. Pastikan curl diaktifkan di php.ini yang benar-benar dimuat worker Anda, karena di Windows baris ekstensi itu terkadang masih diberi tanda komentar.
  • Sebuah koneksi queue. Aplikasi Laravel baru memakai driver database, dan contoh di bawah juga begitu.
  • CapSkip yang berjalan di komputer Windows. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk mesin itu, sehingga cocok untuk worker di PC yang sama. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga aplikasi Laravel di server Linux, Forge, atau host lain mana pun dapat menjangkaunya melalui API. Keduanya diatur di pengaturan koneksi.
# Run in the Laravel project root
composer require capskip/capskip

Langkah 1: daftarkan klien di container

Simpan alamat pemecah CAPTCHA di konfigurasi, bukan di job, agar kode yang sama bisa berjalan dengan pemecah lokal maupun pemecah di server. Tambahkan satu blok ke config/services.php dan binding singleton ke service provider Anda.

// config/services.php
'capskip' => [
    'key' => env('CAPSKIP_API_KEY', 'capskip'),
    'host' => env('CAPSKIP_HOST', '127.0.0.1'),
    'port' => (int) env('CAPSKIP_PORT', 8080),
],

// app/Providers/AppServiceProvider.php, inside register()
$this->app->singleton(\CapSkip\CapSkip::class, fn () => new \CapSkip\CapSkip([
    'apiKey' => config('services.capskip.key'),
    'host' => config('services.capskip.host'),
    'port' => config('services.capskip.port'),
]));

Klien tidak pernah membaca variabel lingkungan dengan sendirinya, itulah sebabnya binding meneruskan setiap nilai. Di bagian lain aplikasi, baca nilai itu lewat config(), bukan env(): begitu Anda menjalankan config:cache, env() di luar file konfigurasi mengembalikan null.

Langkah 2: pecahkan dan kirim dalam satu job

Beri type-hint klien pada handle() dan Laravel akan meng-inject singleton-nya. Job ini membaca sitekey dari halaman, memecahkan CAPTCHA, lalu mengirim form.

<?php

namespace App\Jobs;

use CapSkip\CapSkip;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Http;

class SubmitSignup implements ShouldQueue
{
    use Queueable;

    public function __construct(public string $pageUrl, public array $fields) {}

    public function handle(CapSkip $solver): void
    {
        $html = Http::timeout(30)->get($this->pageUrl)->throw()->body();
        preg_match('/data-sitekey="([^"]+)"/', $html, $m);

        // Solve and submit in one job: the token expires in two minutes.
        $token = $solver->recaptcha($m[1], $this->pageUrl)['code'];

        Http::asForm()->timeout(30)
            ->post($this->pageUrl, $this->fields + ['g-recaptcha-response' => $token])
            ->throw();
    }
}

Dispatch job ini dengan halaman dan field lain dari form, ke koneksi dan queue tersendiri, yang didefinisikan pada langkah berikutnya:

SubmitSignup::dispatch('https://example.com/signup', ['email' => 'YOUR_EMAIL'])
    ->onConnection('captcha')
    ->onQueue('captcha');

Satukan pemecahan dan pengiriman. Token reCAPTCHA berlaku sekitar dua menit, dan token yang diserahkan ke job kedua dalam sebuah rantai bisa menghabiskan waktu itu untuk menunggu di belakang pekerjaan lain. Artikel tentang berapa lama token reCAPTCHA tetap valid membahas batas waktu itu secara rinci. Klien PHP bersifat sinkron, jadi proses worker sibuk selama seluruh pemecahan; cara menjalankan beberapa pemecahan secara berdampingan di Laravel adalah menambah proses worker, bukan memakai klien lain.

Langkah 3: atur urutan timeout

Ada empat timer yang berjalan pada job CAPTCHA queue Laravel. Dari dalam ke luar:

  • Pengaturan milik CapSkip sendiri. Tugas reCAPTCHA bisa menunggu hingga 250 detik untuk thread yang kosong (Wait Timeout) dan mendapat 250 detik untuk dipecahkan (Row Timeout). Jika salah satunya habis, CapSkip menggagalkan tugas itu dan klien memunculkan ApiException, kecuali batas milik klien sendiri di bawah sudah terpicu lebih dulu.
  • Polling oleh klien. recaptcha() melakukan polling hingga recaptchaTimeout, 300 detik secara default, lalu memunculkan TimeoutException. Dengan dua panggilan HTTP di sekitarnya yang masing-masing dibatasi 30 detik di atas, seluruh handle() selesai dalam waktu paling lama sekitar enam menit ketika CapSkip menjawab dengan normal. Namun, batas 300 detik itu diperiksa di antara polling, dan setiap permintaan yang dikirim klien bisa menggantung hingga 120 detik, jadi CapSkip yang berhenti menjawab bisa membuat job berjalan lebih lama dari itu.
  • Timeout job. Opsi timeout milik worker bernilai default 60 detik, dan job yang berjalan lebih lama akan membuat proses worker-nya dimatikan. Setel $timeout pada job dengan nilai yang lebih panjang dari enam menit di atas.
  • retry_after. Setiap koneksi queue mengembalikan job yang sudah di-reserve ke queue begitu job itu sudah berjalan selama durasi ini, 90 detik secara default. Jika job masih punya sisa percobaan, pemecahan lambat yang masih berlangsung di satu worker lalu dimulai lagi di worker lain, yang berarti pemecahan kedua dan pengiriman form kedua. Jika tidak ada sisa, worker kedua langsung menandainya gagal, sementara worker pertama mungkin tetap melanjutkan hingga mengirim form.

Dokumentasi Laravel menyatakan bahwa timeout harus selalu setidaknya beberapa detik lebih pendek daripada retry_after. Jadi urutannya: klien lebih dulu, timeout job kedua, retry_after terakhir. Beri job CAPTCHA koneksi tersendiri, agar retry_after yang panjang tidak memperlambat pemulihan semua job lain di aplikasi:

// config/queue.php, under 'connections'
'captcha' => [
    'driver' => 'database',
    'connection' => env('DB_QUEUE_CONNECTION'),
    'table' => env('DB_QUEUE_TABLE', 'jobs'),
    'queue' => 'captcha',
    // Longer than the job's timeout, so no second worker takes it.
    'retry_after' => 420,
    'after_commit' => false,
],

// On the job class
public $timeout = 390;

Jalankan worker untuk koneksi dan queue itu:

php artisan queue:work captcha --queue=captcha

$timeout pada job lebih diutamakan daripada opsi timeout milik worker, jadi perintah itu tidak memerlukan opsi tersebut.

Langkah 4: coba ulang hanya yang bisa diperbaiki dengan retry

Job mendapat satu percobaan kecuali Anda menentukan lain, jadi exception apa pun membuatnya gagal permanen. Sebagian kegagalan CAPTCHA layak dicoba lagi: hasil ERROR_CAPTCHA_UNSOLVABLE, TimeoutException, atau NetworkException karena CapSkip sedang restart. Yang lain tidak akan pernah berhasil jika diulang: API key yang salah, sitekey yang formatnya salah (ERROR_GOOGLEKEY), atau parameter yang ditolak klien. Sitekey yang ditolak Google kembali sebagai ERROR_CAPTCHA_UNSOLVABLE, sehingga menghabiskan jatah retry seperti hasil tidak bisa dipecahkan lainnya. Izinkan tiga percobaan, beri jeda di antaranya, dan langsung gagalkan kasus yang tidak ada harapan.

use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\ValidationException;

// On the job class
public $tries = 3;
public $backoff = [30, 120];

// Inside handle(), around the solve
try {
    $token = $solver->recaptcha($m[1], $this->pageUrl)['code'];
} catch (ValidationException $e) {
    $this->fail($e);   // a bad parameter will not fix itself
    return;
} catch (ApiException $e) {
    if (! str_contains($e->getMessage(), 'UNSOLVABLE')) {
        $this->fail($e);   // a wrong API key and the like
        return;
    }
    throw $e;   // retried after the backoff
}

$this->fail() marks the job failed without spending the remaining attempts, and anything rethrown is retried after 30 seconds, then 120. A job killed by its timeout also uses up an attempt, but it is picked up again only when retry_after expires, not after the backoff. A 4xx from the form also throws, and the retry solves and posts again. If the site answers 4xx for bad input, catch Illuminate\Http\Client\RequestException around the post and call $this->fail($e) when $e->response->clientError() is true.

Menjalankan worker di Windows

Menjalankan Laravel di PC Windows yang sama dengan CapSkip adalah penyiapan paling sederhana, dan ada satu jebakannya. Laravel menegakkan timeout job dengan ekstensi pcntl, dan pcntl tidak ada di Windows. Perintah ini mencetak bool(false) di sana:

php -r "var_dump(extension_loaded('pcntl'));"

Tanpa pcntl, $timeout milik job dan opsi timeout milik worker diabaikan diam-diam, dan job berjalan sampai selesai, berapa pun lamanya. retry_after tetap berlaku, karena nilai itu diperiksa saat worker berikutnya mengambil job, bukan ditegakkan lewat sinyal. Jadi di Windows, tidak ada apa pun di Laravel yang menghentikan job yang lambat. Wait Timeout dan Row Timeout milik CapSkip sendiri serta batas polling 300 detik milik klien itulah yang mengakhiri pemecahan, jadi biarkan semuanya tetap berlaku. Jaga retry_after di atas durasi terlama sebuah job: 420 mencakup CapSkip yang menjawab dengan normal, dan 660 juga mencakup koneksi yang macet, misalnya lewat mode Server melintasi internet.

Ada dua hal lain yang berbeda. Laravel Horizon membutuhkan ekstensi pcntl dan posix, jadi Horizon tidak bisa diinstal di Windows; gunakan queue:work biasa di sana, atau jalankan Horizon di host Linux yang memanggil CapSkip dalam mode Server. Selain itu, tidak ada Supervisor, jadi jalankan setiap worker di bawah scheduled task atau service wrapper yang me-restart-nya. Di Linux, stopwaitsecs milik Supervisor harus lebih panjang dari job terlama Anda, atau deploy akan mematikan pemecahan di tengah jalan. Dalam kedua kasus, setelah deploy atau perubahan .env, jalankan php artisan config:cache jika Anda meng-cache konfigurasi, lalu php artisan queue:restart, karena worker tetap memakai konfigurasi yang dimuatnya saat boot.

Setiap proses worker memecahkan satu CAPTCHA pada satu waktu, dan CapSkip menjalankan hingga 10 pemecahan reCAPTCHA sekaligus secara default (Max. Threads di pengaturan reCAPTCHA-nya). Lebih dari sekitar sepuluh worker pada queue captcha hanya membuat job menunggu di dalam CapSkip, dan waktu tunggu itu dihitung terhadap Wait Timeout.

Contoh lengkap yang berfungsi

<?php

namespace App\Jobs;

use CapSkip\CapSkip;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\ValidationException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Throwable;

class SubmitSignup implements ShouldQueue
{
    use Queueable;

    public $tries = 3;
    public $backoff = [30, 120];
    // GET 30 s + solve up to 300 s + POST 30 s, plus a margin.
    public $timeout = 390;

    public function __construct(public string $pageUrl, public array $fields) {}

    public function handle(CapSkip $solver): void
    {
        $html = Http::timeout(30)->get($this->pageUrl)->throw()->body();
        if (! preg_match('/data-sitekey="([^"]+)"/', $html, $m)) {
            $this->fail(new RuntimeException("No sitekey on {$this->pageUrl}"));
            return;
        }

        try {
            $token = $solver->recaptcha($m[1], $this->pageUrl)['code'];
        } catch (ValidationException $e) {
            $this->fail($e);
            return;
        } catch (ApiException $e) {
            if (! str_contains($e->getMessage(), 'UNSOLVABLE')) {
                $this->fail($e);
                return;
            }
            throw $e;
        }

        // Submit in the same job, while the token is still valid.
        Http::asForm()->timeout(30)
            ->post($this->pageUrl, $this->fields + ['g-recaptcha-response' => $token])
            ->throw();
    }

    public function failed(?Throwable $exception): void
    {
        logger()->warning('Signup gave up', [
            'url' => $this->pageUrl,
            'error' => $exception?->getMessage(),
        ]);
    }
}

Pasangkan dengan koneksi captcha dari Langkah 3 dan binding dari Langkah 1, dispatch seperti yang ditunjukkan di Langkah 2, lalu jalankan worker dengan perintah dari Langkah 3. Pola sitekey mengharapkan atribut data-sitekey dengan tanda kutip ganda, seperti yang ditulis snippet resmi reCAPTCHA. Untuk widget invisible atau Enterprise, berikan opsi yang sesuai ke recaptcha(), seperti dijelaskan di halaman pemecah reCAPTCHA v2; endpoint mentah di balik pemanggilan itu ada di referensi API.

Kesalahan umum dan artinya

Apa yang Anda lihatPenyebabPerbaiki
"has timed out" setelah 60 detik, dan job ditandai gagalTimeout default worker adalah 60 detik dan job hanya punya satu percobaanSetel $timeout pada job, dan $tries jika Anda menginginkan retry
Form terkirim dua kali, atau dua pemecahan berjalan untuk satu jobJob berjalan lebih lama dari retry_after, 90 detik secara default, dan worker kedua mengambilnyaTempatkan job CAPTCHA di koneksi dengan retry_after yang lebih panjang dari $timeout
Di Windows, job berjalan jauh melewati $timeout-nyaTimeout job membutuhkan pcntl, yang tidak dimiliki WindowsAndalkan timeout milik CapSkip sendiri dan batas 300 detik milik klien, dan jaga retry_after di atas durasi seluruh job
"has been attempted too many times"Job diambil lagi setelah worker-nya mati atau melewati retry_after, lebih sering daripada yang diizinkan $triesCari worker yang dimatikan di tengah job oleh deploy, dan job yang berjalan lebih lama dari retry_after
ApiException yang berisi ERROR_CAPTCHA_UNSOLVABLECapSkip menggagalkan tugas itu, misalnya ketika tugas berjalan melewati Row TimeoutLempar ulang agar Laravel mencoba lagi setelah backoff
ApiException yang berisi ERROR_KEY_DOES_NOT_EXISTValidasi API key aktif di CapSkip dan CAPSKIP_API_KEY tidak cocok dengan key mana pun di sanaPerbaiki key dan restart worker; gagalkan job alih-alih mencobanya ulang
NetworkException pada pemecahan pertama setelah mengubah .envWorker masih berjalan dengan host lama, atau CapSkip tidak berjalanJalankan php artisan config:cache jika Anda meng-cache konfigurasi, lalu php artisan queue:restart, dan periksa mode Local atau Server
Call to undefined function curl_init()Ekstensi curl nonaktif di php.ini yang dimuat workerAktifkan extension=curl di php.ini tersebut
Permintaan halaman mengalami timeout selagi job "berjalan"Job di-dispatch tanpa koneksi captcha dan QUEUE_CONNECTION bernilai sync, sehingga job berjalan di dalam permintaan webDispatch ke koneksi captcha seperti di Langkah 2, dan jalankan worker di koneksi itu
Situs menolak tokenToken dikirim setelah kedaluwarsa, atau ke URL yang salahKirim dari job yang sama segera setelah pemecahan, ke URL action milik form

FAQ

Mengapa tidak memecahkan CAPTCHA di dalam controller?

Karena pemecahan memakan waktu puluhan detik dan bisa sampai beberapa menit. Itu lebih lama daripada kesabaran pengunjung dan lebih lama dari max_execution_time 30 detik yang diberikan sebagian besar instalasi PHP untuk permintaan web. Dengan memasukkan job ke queue, controller bisa langsung mengembalikan respons, sementara bagian yang lambat berjalan di proses worker yang tidak punya batas seperti itu.

Apakah job CAPTCHA queue Laravel bisa berjalan di bawah Horizon?

Bisa, di host yang memiliki pcntl, yaitu Linux atau macOS. Horizon menyetel timeout per supervisor di config/horizon.php, jadi beri supervisor yang menjalankan queue captcha timeout sedikit di atas 390 detik milik job, misalnya 400, karena dengan strategi auto balancing, Horizon menghentikan worker yang berjalan melewati timeout-nya sendiri saat melakukan scale down. Jaga retry_after pada koneksi Redis di atas angka itu (420 sudah cukup), dan arahkan CAPSKIP_HOST ke mesin Windows yang menjalankan CapSkip dalam mode Server.

Bisakah aplikasi Laravel di server Linux memakai CapSkip di PC Windows saya?

Bisa. Alihkan CapSkip ke mode Server di pengaturan koneksi agar ia mendengarkan di alamat jaringan, setel CAPSKIP_HOST di .env aplikasi, lalu restart worker. Gunakan IP publik statis dan aturan firewall yang hanya mengizinkan server Anda jika rutenya melewati internet, dan aktifkan validasi API key. Pemecah CAPTCHA tetap berada di mesin Windows milik Anda sendiri, jadi tidak ada yang berubah soal cara pemecahan dihitung.

Bagaimana perbandingannya dengan Hangfire atau Celery?

Bentuknya sama di mana pun: pecahkan dan kirim dalam satu job, coba ulang hanya kegagalan yang bisa diperbaiki dengan retry, dan sesuaikan jumlah worker dengan jumlah thread CapSkip. Yang berbeda adalah default yang menjebak. Laravel memberi satu percobaan dan menyerahkan job yang lambat ke worker kedua, yang lalu menggagalkannya atau, begitu Anda mengizinkan retry, menjalankannya lagi; Hangfire mencoba ulang sepuluh kali selama berjam-jam. Versi .NET-nya ada di panduan job CAPTCHA Hangfire.

Versi singkatnya

Untuk job CAPTCHA queue Laravel, bind klien CapSkip di container, lalu pecahkan dan kirim di job yang sama. Beri job CAPTCHA koneksi tersendiri dengan retry_after 420 detik, setel $timeout ke 390 dan $tries ke 3 dengan backoff, dan gagalkan error yang tidak bisa diperbaiki dengan retry. Di Windows, ingat bahwa tidak ada timeout yang ditegakkan tanpa pcntl, dan restart worker setelah setiap deploy.

Manfaat pengaturan ini paling terasa saat retry. Job yang mencoba ulang CAPTCHA yang tidak bisa dipecahkan sebanyak dua kali adalah hal yang biasa ketika pemecah captcha berjalan di mesin Anda sendiri, karena setiap percobaan tambahan hanya memakan waktu worker dan thread di sana, bukan satu pemecahan lagi yang ditagih per pemakaian.