Tutorial

Menyimpan Token CAPTCHA untuk Digunakan Kembali

Memecahkan CAPTCHA membutuhkan waktu dan uang. Jika token yang sama dapat digunakan kembali dalam jangka waktu validitasnya, caching menghilangkan panggilan API yang berlebihan. Panduan ini mencakup token mana yang dapat di-cache, berapa lama bertahan, dan cara menerapkan caching dengan aman.


Masa Pakai Token Berdasarkan Tipe CAPTCHA

Tipe CAPTCHA Masa pakai token Dapat di-cache? Catatan
reCAPTCHA v2 ~120 detik Terbatas Umumnya sekali pakai per situs
reCAPTCHA v3 ~120 detik Terbatas Skor bisa bervariasi per permintaan
reCAPTCHA Enterprise ~120 detik Tidak Spesifik tindakan, sekali pakai
Cloudflare Turnstile ~300 detik Ya, dalam jendela waktu Token bisa digunakan kembali hingga kedaluwarsa
Cloudflare Challenge qa_validation_cookie ~15–30 menit Ya Cookie bisa digunakan kembali untuk sesi
Image OCR N/A (hasil teks) Ya Hasil tidak pernah kedaluwarsa
GeeTest v3 ~60 detik Tidak Spesifik tantangan

Wawasan utama: Cloudflare Challenge (qa_validation_cookie) dan Image OCR adalah yang paling mudah di-cache. Token reCAPTCHA memiliki jendela pendek dan sering kali hanya sekali pakai.


Saat cache berfungsi

Caching efektif ketika:

  1. Halaman yang sama, beberapa permintaan – misalnya, mengirimkan formulir yang sama beberapa kali
  2. Cloudflare qa_validation_cookie — satu penyelesaian akan membuka seluruh sesi
  3. OCR Massal — gambar yang sama muncul berulang kali (misalnya, CAPTCHA statis)
  4. validasi awal - selesaikan token sebelum dibutuhkan

Caching tidak berfungsi ketika:

  • Situs ini memvalidasi setiap token hanya sekali
  • Token terikat pada tindakan atau sesi tertentu
  • Token sudah habis masa berlakunya

Python - cache dalam memori

import time
import hashlib
from typing import Optional
import requests

SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"


class TokenCache:
    def __init__(self):
        self.cache = {}

    def _key(self, method: str, params: dict) -> str:
        # Cache key from method + stable params
        stable = {k: v for k, v in sorted(params.items())
                  if k not in ("key", "json")}
        raw = f"{method}:{stable}"
        return hashlib.sha256(raw.encode()).hexdigest()[:16]

    def get(self, method: str, params: dict) -> Optional[str]:
        key = self._key(method, params)
        entry = self.cache.get(key)
        if entry and entry["expires_at"] > time.time():
            print(f"Cache HIT: {key}")
            return entry["token"]
        if entry:
            del self.cache[key]
        return None

    def set(self, method: str, params: dict, token: str, ttl: int):
        key = self._key(method, params)
        self.cache[key] = {
            "token": token,
            "expires_at": time.time() + ttl,
        }
        print(f"Cached: {key} (TTL: {ttl}s)")

    def invalidate(self, method: str, params: dict):
        key = self._key(method, params)
        self.cache.pop(key, None)

    def cleanup(self):
        now = time.time()
        expired = [k for k, v in self.cache.items() if v["expires_at"] <= now]
        for k in expired:
            del self.cache[k]


# TTL per CAPTCHA type
TTL_MAP = {
    "userrecaptcha": 100,       # 120s lifetime, 20s safety margin
    "turnstile": 240,           # 300s lifetime, 60s margin
    "cloudflare_challenge": 900,# 15min lifetime, 5min margin
    "base64": 86400,            # OCR result never expires — cache 24h
}


class CachedSolver:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.cache = TokenCache()

    def solve(self, method: str, params: dict) -> str:
        # Check cache first
        cached = self.cache.get(method, params)
        if cached:
            return cached

        # Solve via API
        token = self._api_solve(method, params)
        ttl = TTL_MAP.get(method, 60)
        self.cache.set(method, params, token, ttl)
        return token

    def _api_solve(self, method: str, params: dict) -> str:
        data = {
            "key": self.api_key,
            "method": method,
            "json": 1,
            **params
        }

        resp = requests.post(SUBMIT_URL, data=data, timeout=15)
        result = resp.json()

        if result.get("status") != 1:
            raise Exception(result.get("error_text", result.get("request")))

        task_id = result["request"]
        return self._poll(task_id)

    def _poll(self, task_id: str, max_wait: int = 120) -> str:
        elapsed = 0
        while elapsed < max_wait:
            time.sleep(5)
            elapsed += 5

            resp = requests.get(RESULT_URL, params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            }, timeout=10)
            result = resp.json()

            if result.get("status") == 1:
                return result["request"]
            if result.get("request") == "CAPCHA_NOT_READY":
                continue

            raise Exception(result.get("error_text", result.get("request")))

        raise Exception(f"Timeout: {task_id}")


# Usage
solver = CachedSolver(api_key="YOUR_API_KEY")

# First call — hits API
token1 = solver.solve("turnstile", {
    "sitekey": "0x4AAAA-SITEKEY",
    "pageurl": "https://example.com"
})
print(f"Token 1: {token1[:40]}...")

# Second call within TTL — cache hit, no API call
token2 = solver.solve("turnstile", {
    "sitekey": "0x4AAAA-SITEKEY",
    "pageurl": "https://example.com"
})
print(f"Token 2: {token2[:40]}...")
print(f"Same token: {token1 == token2}")  # True

Node.js — cache dalam memori

const axios = require("axios");
const crypto = require("crypto");

const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

const TTL_MAP = {
  userrecaptcha: 100,
  turnstile: 240,
  cloudflare_challenge: 900,
  base64: 86400,
};

class TokenCache {
  constructor() {
    this.cache = new Map();
  }

  _key(method, params) {
    const stable = Object.entries(params)
      .filter(([k]) => k !== "key" && k !== "json")
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([k, v]) => `${k}=${v}`)
      .join("&");
    return crypto.createHash("sha256").update(`${method}:${stable}`).digest("hex").slice(0, 16);
  }

  get(method, params) {
    const key = this._key(method, params);
    const entry = this.cache.get(key);
    if (entry && entry.expiresAt > Date.now()) {
      console.log(`Cache HIT: ${key}`);
      return entry.token;
    }
    if (entry) this.cache.delete(key);
    return null;
  }

  set(method, params, token, ttlMs) {
    const key = this._key(method, params);
    this.cache.set(key, { token, expiresAt: Date.now() + ttlMs });
    console.log(`Cached: ${key} (TTL: ${ttlMs / 1000}s)`);
  }
}

class CachedSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.cache = new TokenCache();
  }

  async solve(method, params) {
    const cached = this.cache.get(method, params);
    if (cached) return cached;

    const token = await this._apiSolve(method, params);
    const ttl = (TTL_MAP[method] || 60) * 1000;
    this.cache.set(method, params, token, ttl);
    return token;
  }

  async _apiSolve(method, params) {
    const resp = await axios.post(SUBMIT_URL, null, {
      params: { key: this.apiKey, method, json: 1, ...params },
      timeout: 15000,
    });

    if (resp.data.status !== 1) {
      throw new Error(resp.data.error_text || resp.data.request);
    }

    return this._poll(resp.data.request);
  }

  async _poll(taskId, maxWait = 120000) {
    let elapsed = 0;
    while (elapsed < maxWait) {
      await new Promise((r) => setTimeout(r, 5000));
      elapsed += 5000;

      const resp = await axios.get(RESULT_URL, {
        params: { key: this.apiKey, action: "get", id: taskId, json: 1 },
        timeout: 10000,
      });

      if (resp.data.status === 1) return resp.data.request;
      if (resp.data.request === "CAPCHA_NOT_READY") continue;

      throw new Error(resp.data.error_text || resp.data.request);
    }
    throw new Error("Timeout");
  }
}

// Usage
(async () => {
  const solver = new CachedSolver("YOUR_API_KEY");

  const token1 = await solver.solve("turnstile", {
    sitekey: "0x4AAAA-SITEKEY",
    pageurl: "https://example.com",
  });
  console.log(`Token 1: ${token1.slice(0, 40)}...`);

  const token2 = await solver.solve("turnstile", {
    sitekey: "0x4AAAA-SITEKEY",
    pageurl: "https://example.com",
  });
  console.log(`Token 2: ${token2.slice(0, 40)}...`);
  console.log(`Same token: ${token1 === token2}`);
})();

Redis cache untuk sistem terdistribusi

Untuk penyiapan multipekerja, gunakan Redis alih-alih cache dalam memori:

import redis
import json

r = redis.Redis(host="localhost", port=6379, db=0)

def cache_token(method, params, token, ttl):
    key = f"captcha:{method}:{hash(frozenset(params.items()))}"
    r.setex(key, ttl, token)

def get_cached_token(method, params):
    key = f"captcha:{method}:{hash(frozenset(params.items()))}"
    return r.get(key)

Redis secara otomatis menangani masa berlaku TTL dan bekerja di berbagai proses.


Pola validasi awal

Pecahkan token sebelum dibutuhkan. Simpan buffer token siap pakai:

from collections import deque
from threading import Thread

token_buffer = deque(maxlen=5)

def pre_solve_worker(solver, method, params):
    while True:
        if len(token_buffer) < 3:
            try:
                token = solver._api_solve(method, params)
                ttl = TTL_MAP.get(method, 60)
                token_buffer.append({
                    "token": token,
                    "expires_at": time.time() + ttl
                })
            except Exception as e:
                print(f"validasi awal di staging failed: {e}")
        time.sleep(2)

# Start validasi staging in background
thread = Thread(
    target=pre_solve_worker,
    args=(solver, "turnstile", {"sitekey": "0x4AAAA-KEY", "pageurl": "https://example.com"}),
    daemon=True
)
thread.start()

# Consume token QA staging
def get_presolved():
    while token_buffer:
        entry = token_buffer.popleft()
        if entry["expires_at"] > time.time():
            return entry["token"]
    return None

Aturan Invalidasi Cache

Pemicu Tindakan
Token ditolak oleh situs target Invalidasi dan solve ulang
TTL sudah kedaluwarsa Dihapus otomatis dari cache
Proxy berubah Invalidasi token Cloudflare (terikat IP)
Konfigurasi CAPTCHA situs diperbarui Hapus semua token yang di-cache untuk situs tersebut

Pemecahan Masalah

Masalah Penyebab Solusi
Token yang di-cache ditolak Token kedaluwarsa atau sekali pakai Kurangi TTL atau nonaktifkan cache untuk tipe tersebut
Cache tidak pernah hit Params berbeda antar panggilan Normalisasi param sebelum di-hash
Token basi di Redis TTL terlalu panjang Turunkan TTL dengan safety margin
Memory terus tumbuh Tidak ada pembersihan Panggil cleanup() secara berkala atau gunakan Redis dengan TTL

Pertanyaan Umum

Bisakah saya menyimpan token reCAPTCHA v2 dalam cache?

Terkadang. Banyak situs hanya menerima token satu kali. Uji dengan mengirimkan token yang sama dua kali – jika pengiriman kedua berhasil, cache akan berfungsi untuk situs tersebut.

Berapa banyak yang bisa dihemat dari cache?

Untuk Cloudflare Challenge, satu penyelesaian dapat mencakup keseluruhan sesi 15–30 menit. Hal ini dapat mengurangi biaya sebesar 90%+ untuk scraping frekuensi tinggi pada domain yang sama.

Apakah penyelesaian awal layak dilakukan?

Ya, jika saluran pipa Anda memiliki permintaan yang dapat diprediksi. Penyelesaian awal menghilangkan waktu tunggu dengan mengorbankan potensi pemborosan token jika permintaan turun.


Optimalkan Biaya CAPTCHA dengan CaptchaAI

Mulai menyimpan token dalam cache di captchaai.com.

Panduan Terkait

Komentar dinonaktifkan untuk artikel ini.