Cara Memecahkan CAPTCHA di Background Job Hangfire (.NET)

Untuk memecahkan CAPTCHA di background job Hangfire, panggil klien .NET CapSkip di dalam metode job, kirim token di job yang sama, dan teruskan CancellationToken milik Hangfire ke pemecahan. Bagian itu hanya butuh sepuluh baris. Pekerjaan sebenarnya ada pada dua default Hangfire yang cocok untuk job biasa, tetapi tidak untuk job captcha Hangfire: sepuluh retry otomatis untuk exception apa pun, yang tersebar selama sekitar empat setengah jam, dan pool berisi hingga dua puluh worker yang tetap ditempati pemecahan async dari awal sampai akhir. Panduan ini membahas job-nya, aturan retry yang tahu kegagalan mana yang layak diulang, queue yang ukurannya disesuaikan dengan CapSkip, shutdown, dan menjalankan Hangfire di mesin Windows yang sama dengan pemecah CAPTCHA.
Apa yang Anda butuhkan
- CapSkip yang berjalan di komputer Windows. Hangfire bisa berjalan di mesin yang sama atau memanggilnya dari mesin lain.
- Hangfire 1.8 dengan storage apa pun. Contoh kodenya memakai storage SQL Server, pilihan yang umum di Windows, dan filter retry di bawah membutuhkan 1.8.0 atau lebih baru.
- Paket NuGet CapSkip. Setiap metode pemecahan menerima CancellationToken opsional, dan itulah yang memungkinkan shutdown menghentikan pemecahan dengan bersih.
- .NET 8 atau lebih baru. Contoh kodenya memakai primary constructor, yang hadir bersama C# 12.
- Sebuah alamat untuk pemecah CAPTCHA. Mode Local menjawab di 127.0.0.1 hanya untuk perangkat itu; mode Server mendengarkan di alamat jaringan atau IP publik Anda sehingga Hangfire di mesin lain dapat memanggilnya melalui API. Keduanya ada di pengaturan koneksi.
# dotnet add package CapSkip dotnet add package CapSkip dotnet add package Hangfire.NetCore dotnet add package Hangfire.SqlServer dotnet add package Microsoft.Data.SqlClient
Hangfire.NetCore membawa Hangfire.Core serta metode registrasi AddHangfire dan AddHangfireServer. Aplikasi web yang juga menginginkan dashboard menambahkan Hangfire.AspNetCore sebagai gantinya. Hangfire.SqlServer 1.8 dirilis tanpa SQL client bawaan, itulah sebabnya Microsoft.Data.SqlClient ada di daftar.
Langkah 1: pecahkan dan kirim dalam satu job
Taruh pemecahan dan pengiriman form di dalam satu metode job; setiap job captcha Hangfire dalam panduan ini dibangun di atas aturan itu. Token reCAPTCHA hanya berlaku sekitar dua menit, jadi pengiriman tidak boleh menunggu di queue di belakang pekerjaan lain, dan justru itulah yang akan dilakukan continuation yang dibuat dengan ContinueJobWith. Hal yang sama berlaku untuk retry: setiap eksekusi harus memecahkan dari awal, jangan pernah melanjutkan dari token atau id captcha yang disimpan oleh percobaan sebelumnya.
// dotnet add package CapSkip
using CapSkip;
public class SignupJob(CapSkipClient solver, HttpClient http)
{
public async Task RunAsync(string pageUrl, string sitekey, CancellationToken ct)
{
// Solve and post together: the token lasts about 2 minutes.
var result = await solver.RecaptchaAsync(sitekey, pageUrl, cancellationToken: ct);
var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["g-recaptcha-response"] = result.Code,
});
// No ct here: once the submit starts, let it finish.
var response = await http.PostAsync(pageUrl, form);
response.EnsureSuccessStatusCode(); // a rejected post fails the job
}
}Masukkan job ke queue dengan nilai biasa. Hangfire menserialisasi argumen ke storage, jadi berikan string, jangan pernah kliennya, dan berikan CancellationToken.None sebagai placeholder. Hangfire menggantinya dengan token yang sebenarnya tepat sebelum job berjalan.
BackgroundJob.Enqueue<SignupJob>(job =>
job.RunAsync("https://example.com/signup", "YOUR_SITEKEY", CancellationToken.None));Hangfire membangun SignupJob dari service container Anda, jadi daftarkan klien CapSkip sebagai singleton dan beri job itu typed HttpClient, seperti pada contoh lengkap. Klien tidak menyimpan apa pun selain pengaturannya, jadi satu instance aman dipakai bersama oleh semua worker. Field form di sini hanya ilustrasi; kirim apa pun yang benar-benar dikirim form target.
Langkah 2: ganti aturan retry default
Hangfire menerapkan filter retry otomatis ke setiap job. Secara default, filter itu mencoba ulang exception apa pun sebanyak sepuluh kali, dan jeda sebelum retry ke-n adalah (n dikurangi 1) pangkat empat dalam detik, ditambah 15, ditambah angka acak, sehingga totalnya sekitar empat setengah jam antara kegagalan pertama dan terakhir. Itu cocok untuk server email yang tidak stabil. Itu tidak cocok untuk job CAPTCHA, yang sebagian kegagalannya layak dicoba sekali lagi sementara yang lain akan gagal dengan cara yang persis sama setiap kali.
| Yang dilempar SDK | Penyebab umum | Layak dicoba ulang? |
|---|---|---|
| CapSkip.TimeoutException | Pemecahan melewati recaptchaTimeout, 300 detik secara default, atau CapSkip tidak bisa dijangkau atau restart saat SDK melakukan polling | Ya |
| NetworkException | CapSkip tidak bisa dijangkau saat job mengirim tugasnya | Ya |
| ApiException dengan ERROR_CAPTCHA_UNSOLVABLE | Percobaan itu gagal, atau kehabisan waktu di dalam CapSkip; percobaan berikutnya mungkin tidak | Ya |
| ApiException dengan kode lain apa pun | Sitekey atau URL halaman yang formatnya salah, atau API key yang ditolak CapSkip | Tidak, gagalnya selalu dengan cara yang sama |
| ValidationException | Kode Anda mengirim opsi yang tidak diterima metode itu | Tidak, itu bug |
Satu keterbatasan tabel itu: CapSkip hanya bisa memeriksa apakah format sitekey benar. Sitekey yang formatnya benar tetapi ditolak Google tetap terbaca sebagai ERROR_CAPTCHA_UNSOLVABLE, sehingga job menghabiskan jatah retry-nya sebelum gagal.
Hangfire 1.8 menambahkan OnlyOn ke atribut retry, yang membatasi retry hanya pada tipe exception yang Anda cantumkan. ApiException mencakup satu baris yang bersifat sementara dan satu baris yang permanen, jadi job ini mengubah jenis yang permanen menjadi tipe exception yang tidak ada di daftar:
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 30, 120, 600 },
OnlyOn = new[] { typeof(CapSkip.TimeoutException),
typeof(NetworkException), typeof(ApiException) })]
public async Task RunAsync(string pageUrl, string sitekey, CancellationToken ct)
{
SolveResult result;
try
{
result = await solver.RecaptchaAsync(sitekey, pageUrl, cancellationToken: ct);
}
catch (ApiException ex) when (!ex.Message.Contains("ERROR_CAPTCHA_UNSOLVABLE"))
{
// Not on the OnlyOn list, so Hangfire fails the job at once.
throw new InvalidOperationException($"CapSkip refused the task: {ex.Message}", ex);
}
// ...post the form as in Step 1.
}Attempts menghitung retry, jadi ini berarti satu eksekusi ditambah hingga tiga eksekusi lagi. Job yang kehabisan percobaan, atau melempar sesuatu di luar daftar, masuk ke status Failed dan tetap di sana agar bisa Anda periksa. Tulis CapSkip.TimeoutException secara lengkap: dengan implicit usings pada proyek .NET 6 atau lebih baru, System.TimeoutException juga berada dalam scope, dan nama pendeknya tidak akan bisa dikompilasi. Pesan ApiException adalah balasan mentah dari CapSkip, itulah sebabnya pencocokan pada kode error berhasil, dan daftar lengkap kodenya ada di referensi API.
Langkah 3: beri pemecahan queue dan jumlah worker tersendiri
Server Hangfire menjalankan Environment.ProcessorCount dikali 5 worker, dengan batas maksimum 20. Membuat job menjadi async tidak membebaskan worker selama pemecahan menunggu: Hangfire menjalankan setiap job di thread worker-nya dan menunggu di sana sampai task selesai. Jadi dua puluh pemecahan yang sedang berjalan berarti dua puluh worker sibuk selama pemecahan itu berlangsung, dan semua job lain di aplikasi, termasuk email reset kata sandi, menunggu di belakangnya.
CapSkip punya batasnya sendiri. Max. Threads untuk reCAPTCHA bernilai default 10 di pengaturan aplikasi, dan tugas yang melebihi angka itu menunggu di dalam CapSkip sampai ada thread yang kosong. Tugas yang menunggu lebih lama dari Wait Timeout reCAPTCHA, 250 detik secara default, digagalkan dengan ERROR_CAPTCHA_UNSOLVABLE. Jadi beri setiap job captcha Hangfire queue tersendiri, dengan jumlah worker yang persis sama dengan jumlah thread CapSkip:
// On the job method, next to [AutomaticRetry]:
[Queue("captcha")]
// In Program.cs: one server for solves, sized to CapSkip...
builder.Services.AddHangfireServer(o =>
{
o.Queues = new[] { "captcha" };
o.WorkerCount = 10; // reCAPTCHA Max. Threads in CapSkip
});
// ...and the usual server for everything else.
builder.Services.AddHangfireServer(o => o.Queues = new[] { "default" });Sekarang lonjakan lima ratus pemecahan akan mengantre di storage Hangfire, tempat Anda bisa melihatnya, dan server ini tidak pernah punya lebih dari sepuluh pemecahan di CapSkip pada saat yang sama. WorkerCount dihitung per server, jadi jika Anda menjalankan server captcha di lebih dari satu proses, bagi Max. Threads di antara proses tersebut. Atribut itu diterapkan lagi setiap kali job dimasukkan ke queue, sehingga retry juga kembali ke queue captcha. Nama queue hanya boleh berisi huruf kecil, angka, garis bawah, dan tanda hubung. Jika Anda menaikkan Max. Threads di CapSkip, naikkan WorkerCount agar sama.
Langkah 4: shutdown, dan di mana Hangfire berjalan
CancellationToken yang diteruskan Hangfire akan terpicu dalam dua kasus: server sedang shutdown, karena service dihentikan, ada deploy, atau IIS melakukan recycle, atau job dihapus atau berubah status di dashboard, yang secara default diperiksa Hangfire setiap lima detik. Menyerahkan token itu ke RecaptchaAsync langsung menghentikan polling. Saat shutdown, Hangfire lalu mengembalikan job ke queue-nya dan menjalankannya lagi setelah restart, dengan pemecahan baru. Saat job dihapus, Hangfire membuangnya. CapSkip menyelesaikan tugas yang ditinggalkan itu dengan sendirinya, dan tidak ada yang mengambil hasilnya.
Ada satu jebakan kecil. Hangfire hanya mengembalikan job ke queue jika job berakhir dengan OperationCanceledException, dan SDK memunculkan persis exception itu selama polling. Tetapi jika token terpicu saat SDK masih mengirim tugas, yaitu permintaan pertamanya ke CapSkip, SDK membungkus pembatalan itu dalam NetworkException. Hangfire lalu memperlakukan eksekusi itu sebagai percobaan yang gagal: dengan aturan dari Langkah 2, Hangfire menjadwalkan retry setelah jeda berikutnya di daftar dan menghabiskan satu dari tiga jatah. Satu klausa catch, yang ditaruh sebelum filter ApiException, mengubahnya kembali menjadi pembatalan:
catch (CapSkipError) when (ct.IsCancellationRequested)
{
// A cancelled submit arrives as NetworkException;
// rethrow as cancellation so Hangfire re-queues the job.
throw new OperationCanceledException(ct);
}Jika proses mati mendadak tanpa shutdown sama sekali, storage SQL Server menyerahkan job ke worker lain begitu invisibility timeout-nya habis, lima menit secara default di Hangfire 1.8. Bagaimanapun juga, Hangfire menjalankan job setidaknya sekali, bukan tepat sekali, itulah sebabnya contoh kode membiarkan pengiriman form yang sudah dimulai tetap selesai alih-alih membatalkannya.
Tempat Anda meng-host server Hangfire sama pentingnya. Di dalam situs IIS, application pool berhenti setelah 20 menit menganggur secara default dan melakukan recycle sesuai jadwal, dan pool yang berhenti berarti tidak ada server Hangfire: pemecahan berulang tidak terpicu sampai permintaan web berikutnya membangunkan situs itu. Pilihannya: setel Start Mode pada pool ke AlwaysRunning dan Idle Time-out-nya ke 0, lalu aktifkan Preload Enabled pada situs (yang membutuhkan fitur Application Initialization milik IIS terinstal), atau jalankan server di Windows Service, seperti yang dilakukan contoh lengkap. Di mesin Windows yang sama dengan CapSkip, mode Local dan 127.0.0.1 sudah cukup.
Antisipasi satu perbedaan di antara keduanya. Service dimulai saat boot, tetapi CapSkip adalah aplikasi desktop yang dimulai saat Anda masuk ke Windows. Setelah reboot tanpa pengawasan, setiap pengiriman gagal dengan NetworkException sampai ada yang masuk ke Windows, dan setiap job menghabiskan jatah retry-nya selama jeda itu. Biarkan mesin itu tetap dalam keadaan login, atau periksa setelah setiap restart.
Jika Hangfire berjalan di tempat lain, seperti server kedua, container host, atau Azure App Service, alihkan CapSkip ke mode Server agar ia mendengarkan di alamat jaringan atau IP publik Anda, lalu arahkan klien ke sana. Gunakan IP publik statis jika rutenya melewati internet, dengan aturan firewall untuk alamat yang Anda harapkan. Perangkatnya tetap milik Anda sendiri dan tetap tanpa kuota. Klien tidak membaca variabel lingkungan dengan sendirinya, jadi baca CAPSKIP_HOST di kode startup Anda dan teruskan ke konstruktor.
Contoh lengkap yang berfungsi
Sebuah worker service yang berjalan sebagai Windows Service, dengan job dari langkah di atas dan jadwal berulang. Inilah semua perintah yang dibutuhkannya:
# dotnet new worker -n CaptchaWorker dotnet new worker -n CaptchaWorker cd CaptchaWorker dotnet add package CapSkip dotnet add package Hangfire.NetCore dotnet add package Hangfire.SqlServer dotnet add package Microsoft.Data.SqlClient dotnet add package Microsoft.Extensions.Hosting dotnet add package Microsoft.Extensions.Hosting.WindowsServices dotnet add package Microsoft.Extensions.Http dotnet add package Newtonsoft.Json
Tiga dari baris itu perlu penjelasan. Microsoft.Data.SqlClient mengenkripsi koneksi secara default, jadi SQL Server lokal tanpa sertifikat tepercaya membutuhkan TrustServerCertificate=true di connection string. Baris Microsoft.Extensions.Hosting menaikkan referensi Hosting bawaan template ke versi yang diharapkan paket Windows Services; tanpa baris itu, restore gagal dengan error package downgrade. Newtonsoft.Json memindahkan dependensi JSON milik Hangfire dari versi lama yang ditandai NuGet sebagai rentan.
// dotnet add package CapSkip
using CapSkip;
using Hangfire;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService();
builder.Services.AddSingleton(new CapSkipClient(
apiKey: Environment.GetEnvironmentVariable("CAPSKIP_API_KEY") ?? "capskip",
host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
port: 8080));
builder.Services.AddHttpClient<SignupJob>();
builder.Services.AddHangfire(cfg => cfg
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(builder.Configuration.GetConnectionString("Hangfire")));
builder.Services.AddHangfireServer(o =>
{
o.Queues = new[] { "captcha" };
o.WorkerCount = 10; // reCAPTCHA Max. Threads in CapSkip
});
var host = builder.Build();
host.Services.GetRequiredService<IRecurringJobManager>().AddOrUpdate<SignupJob>(
"nightly-signup",
job => job.RunAsync("https://example.com/signup", "YOUR_SITEKEY", CancellationToken.None),
Cron.Daily());
host.Run();
public class SignupJob(CapSkipClient solver, HttpClient http)
{
[Queue("captcha")]
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 30, 120, 600 },
OnlyOn = new[] { typeof(CapSkip.TimeoutException),
typeof(NetworkException), typeof(ApiException) })]
public async Task RunAsync(string pageUrl, string sitekey, CancellationToken ct)
{
SolveResult result;
try
{
result = await solver.RecaptchaAsync(sitekey, pageUrl, cancellationToken: ct);
}
catch (CapSkipError) when (ct.IsCancellationRequested)
{
throw new OperationCanceledException(ct);
}
catch (ApiException ex) when (!ex.Message.Contains("ERROR_CAPTCHA_UNSOLVABLE"))
{
throw new InvalidOperationException($"CapSkip refused the task: {ex.Message}", ex);
}
var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["g-recaptcha-response"] = result.Code,
});
var response = await http.PostAsync(pageUrl, form);
response.EnsureSuccessStatusCode();
}
}Service ini hanya memproses queue captcha. Aplikasi web Anda memasukkan job ke storage SQL Server yang sama dan mempertahankan servernya sendiri untuk queue default, atau Anda menambahkan pemanggilan AddHangfireServer kedua di sini seperti di Langkah 3. Begitu aplikasi web juga memasukkan job ini ke queue, pindahkan SignupJob dari Program.cs ke class library yang direferensikan kedua proyek, karena aplikasi web membutuhkan tipe itu untuk memasukkannya ke queue. Proyek ini membutuhkan connection string bernama Hangfire di appsettings.json. Instal service ini dengan sc.exe create, atau New-Service di PowerShell, yang diarahkan ke executable hasil publish.
Ganti RecaptchaAsync dengan TurnstileAsync, FriendlyCaptchaAsync, atau metode pemecahan lain, dan bentuk job-nya tetap sama. Ada dua hal yang berubah sesuai tipenya: sesuaikan WorkerCount dengan pengaturan Max. Threads milik tipe itu di CapSkip, dan kirim token di field yang dipakai tipe itu.
Kesalahan umum dan artinya
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| Job tertahan di Enqueued dan tidak pernah dimulai | Metode itu memiliki [Queue("captcha")] tetapi tidak ada server yang mendengarkan queue tersebut | Tambahkan server yang Queues-nya mencakup captcha |
| Satu job yang bermasalah terus di-retry selama berjam-jam | Filter retry default, sepuluh percobaan untuk exception apa pun | Pakai AutomaticRetry dengan OnlyOn seperti di Langkah 2 |
| Retry dengan pesan The operation was canceled, tepat setelah deploy | Token terpicu saat tugas sedang dikirim, dan SDK membungkusnya | Tambahkan catch pembatalan dari Langkah 4 |
| NetworkException di setiap job | CapSkip tidak berjalan, misalnya setelah reboot tanpa ada yang login, atau Hangfire berada di mesin lain sementara CapSkip dalam mode Local | Jalankan CapSkip; jika Hangfire berada di mesin lain, alihkan CapSkip ke mode Server dan setel CAPSKIP_HOST |
| ERROR_CAPTCHA_UNSOLVABLE, atau CapSkip.TimeoutException, saat beban tinggi | Pemecahan yang sedang berjalan lebih banyak daripada thread CapSkip, sehingga tugas menunggu melewati Wait Timeout | Samakan WorkerCount di server captcha dengan Max. Threads |
| Job berhasil tetapi situs menolak kiriman form | Token kedaluwarsa sebelum dikirim, atau form membutuhkan field lain | Pecahkan dan kirim dalam satu job, lalu tiru form aslinya dari DevTools |
| Pemecahan berulang terlewat di beberapa malam | Application pool IIS sedang menganggur atau sedang recycle | Setel AlwaysRunning, atau jalankan server di Windows Service |
| Build gagal karena TimeoutException yang ambigu | CapSkip dan System sama-sama mendefinisikan nama itu | Tulis CapSkip.TimeoutException secara lengkap |
FAQ
Apakah job async membebaskan worker Hangfire-nya selama CAPTCHA dipecahkan?
Tidak. Hangfire mendukung metode job async, tetapi Hangfire menunggu task yang dikembalikan di thread worker yang memulainya, sehingga worker tetap sibuk selama seluruh pemecahan. Itulah sebabnya, untuk job captcha Hangfire, jumlah worker pada queue-nya adalah angka yang membatasi berapa banyak pemecahan yang berjalan sekaligus, dan mengapa angka itu harus sama dengan jumlah thread yang dimiliki CapSkip.
Bisakah saya memecahkan di satu job dan mengirim di continuation?
Bisa, tetapi sebaiknya tidak. Continuation dimasukkan ke queue seperti job lainnya, jadi ia menunggu di belakang apa pun yang sudah mengantre, sedangkan token reCAPTCHA hanya berlaku sekitar dua menit. Retry pada job kedua juga akan mengirim ulang token kedaluwarsa yang sama. Menyatukan keduanya dalam satu job berarti setiap percobaan memecahkan dari awal dan langsung mengirim. Pembahasan lebih lanjut tentang berapa lama token bertahan ada di panduan kedaluwarsa token reCAPTCHA.
Bisakah Hangfire di Azure atau container Linux memakai pemecah CAPTCHA di PC Windows saya?
Bisa. Sisi Hangfire bisa berjalan di mana pun .NET berjalan; hanya CapSkip yang membutuhkan Windows. Alihkan CapSkip ke mode Server di pengaturan koneksi, setel CAPSKIP_HOST ke alamatnya di tempat Hangfire berjalan, lalu teruskan ke klien. Izinkan koneksi melalui Windows Firewall, dan gunakan IP publik statis jika rutenya melewati internet. Platform hosting punya batasan tersendiri; batasan salah satunya dibahas di panduan Azure Functions.
Bagaimana perbandingannya dengan Celery di Python?
Aturan yang paling penting sama di keduanya: pecahkan dari awal pada setiap percobaan dan kirim dalam unit kerja yang sama. Jebakannya berbeda. Jebakan di Celery berasal dari batas waktu dan pengaturan acknowledgement-nya, sedangkan jebakan di Hangfire berasal dari default retry-nya, worker yang tidak dilepaskan oleh kode async, dan cara pembatalan dilaporkan. Sisi Python dibahas di panduan CAPTCHA Celery.
Versi singkatnya
Di job captcha Hangfire, pecahkan dan kirim dalam satu metode, lalu teruskan CancellationToken milik Hangfire ke pemecahan. Ganti aturan retry default dengan AutomaticRetry dan OnlyOn, dan ubah ApiException yang permanen menjadi exception yang tidak ada di daftar. Beri pemecahan sebuah queue dengan jumlah worker yang sama dengan jumlah thread CapSkip, lempar ulang pengiriman yang dibatalkan sebagai OperationCanceledException agar shutdown mengembalikan job ke queue, dan jalankan server di tempat yang tidak bisa menganggur: Windows Service di samping CapSkip, atau di mana pun dengan mode Server.
- Setiap tipe CAPTCHA yang dipecahkan paket .NET: halaman pemecah C# dan .NET.
- Cara kotak centang reCAPTCHA v2 dipecahkan: halaman pemecah reCAPTCHA v2.
Satu poin terakhir soal retry. Karena pemecah CAPTCHA berjalan di mesin Anda sendiri, sebuah retry hanya memakan beberapa detik dari satu thread, bukan satu pemecahan lagi yang ditagih, jadi bypass captcha yang gagal sekali murah untuk dicoba lagi. Yang sebenarnya diwaspadai aturan retry adalah job yang tidak akan pernah berhasil, dan job seperti itulah yang harus dihentikan lebih awal.
