Captcha token đã hết hạn: nguyên nhân và cách khắc phục
Token captcha là bằng chứng ngắn hạn rằng một thử thách đã được vượt qua. Token PopularCaptcha của Scarnox hết hạn khoảng hai phút sau khi được cấp. Nếu bạn submit token sau khoảng thời gian đó, website sẽ từ chối form, thường kèm thông báo rằng captcha đã hết hạn hoặc không hợp lệ. Cách sửa hầu như luôn liên quan đến thời điểm: giải captcha càng gần lúc submit càng tốt, mỗi token chỉ dùng một lần, và chỉ thử lại khi bạn biết lần đầu chưa được chấp nhận.
Vì sao token hết hạn
Token không phải là một thông tin xác thực vĩnh viễn. Website muốn biết rằng một người (hoặc một bộ giải thay mặt người đó) vừa hoàn thành thử thách, nên nó cho token một thời gian sống ngắn. Đồng hồ bắt đầu chạy khi token được cấp, không phải khi bạn gọi API lần đầu. Mọi việc diễn ra giữa lúc giải và lúc submit đều làm mất đi khoảng hai phút đó.
Các nguyên nhân thường gặp
- Giải quá sớm. Giải trước một lô token và giữ lại để dùng sau là nguyên nhân phổ biến nhất. Khi một token được dùng, nó đã cũ.
- Mã chậm giữa lúc giải và lúc submit. Một lần ghi cơ sở dữ liệu dài, một hàng đợi, hoặc một
time.sleepđặt trước lúc gửi form có thể đẩy token ra khỏi cửa sổ hiệu lực. - Tái sử dụng token. Một token đã được chấp nhận, hoặc đã bị một lần thử thất bại dùng rồi, sẽ không hoạt động lại. Hãy coi mỗi token là dùng một lần.
- Tải lại trang. Nếu trang được làm mới sau khi giải, form có thể phát hành một thử thách mới và vô hiệu hóa token bạn đang giữ.
Cách sửa: giải ngay trước khi submit
Mẫu đáng tin cậy nhất là biến việc giải và submit thành một bước. Yêu cầu một token, đặt nó vào form, gửi form, và không làm gì chậm ở giữa:
import requests
API_KEY = "scarnox_YOURAPIKEY"
CREATE_URL = "https://api.scarnox.com/api/tasks/create"
def fresh_token(sitekey: str, page_url: str) -> str:
r = requests.post(
CREATE_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"captcha_type": "PopularCaptchaTaskProxyless",
"site_key": sitekey,
"site_url": page_url,
},
timeout=185, # lời gọi bị chặn cho đến khi token sẵn sàng
)
data = r.json()
if data.get("errorId") != 0:
raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
return data["solution"]["token"]
def submit_form(page_url: str, sitekey: str, form: dict) -> requests.Response:
token = fresh_token(sitekey, page_url) # giải ngay trước khi gửi
return requests.post(
page_url,
data={**form, "h-captcha-response": token, "g-recaptcha-response": token},
timeout=30,
)
Lưu ý submit_form không giữ token trong cache hay hàng đợi. Nó yêu cầu một token, gửi đi, rồi tiếp tục. Các lần giải thất bại không bị tính phí, nên một yêu cầu thất bại không tốn gì ở phía giải.
Thử lại an toàn
Một số website từ chối token hết hạn bằng một phản hồi rõ ràng. Những website khác trả về một trang bình thường với thông báo lỗi bên trong, nên mã trạng thái một mình không cho bạn biết được điều gì. Hãy xem phản hồi thực tế mà website trả về cho token hết hạn và xây dựng kiểm tra của bạn dựa trên đó. Một vòng lặp thử lại có thể trông như sau:
def submit_with_retry(page_url: str, sitekey: str, form: dict, attempts: int = 2):
for _ in range(attempts):
resp = submit_form(page_url, sitekey, form)
if resp.status_code < 400 and "captcha" not in resp.text.lower():
return resp # được chấp nhận, theo phản hồi của website này
raise RuntimeError("captcha token rejected after retries")
Hãy cẩn thận với việc thử lại trên các form tạo ra thứ gì đó. Nếu lần thử đầu thực sự được chấp nhận nhưng code của bạn đọc sai phản hồi, một lần thử lại có thể tạo ra tài khoản, đơn hàng hoặc tin nhắn trùng lặp. Chỉ thử lại khi bạn chắc chắn lần gửi đầu chưa được chấp nhận, và làm cho kiểm tra cụ thể với website bạn đang làm việc cùng.
Canh thời gian cho đúng
- Đừng giải trong vòng lặp các token bạn định dùng sau. Hãy giải theo từng lần submit.
- Giữ công việc giữa lúc giải và lúc gửi ở mức tối thiểu.
- Dùng timeout khoảng 185 giây cho yêu cầu giải, vì lời gọi bị chặn cho đến khi token sẵn sàng. Một bộ đếm thời gian riêng cho lần gửi form của bạn nên ngắn hơn nhiều so với thời hạn của token.
- Nếu bạn cần một pipeline nhanh hơn, xem tốc độ giải captcha, trình bày cách giải song song mà không lưu token.
Các lỗi thường gặp
- Cache token trong dictionary hoặc Redis để dùng sau. Cache trông vô hại cho đến khi một token được dùng sau khi hết cửa sổ hiệu lực và mọi lần submit đều thất bại.
- Giải một lần cho mỗi phiên. Một phiên sống lâu vẫn cần một token mới cho mỗi lần gửi form, vì mỗi lần gửi có thể chạy một kiểm tra thử thách riêng.
- Ngủ giữa lúc giải và lúc submit. Một bộ giới hạn tần suất hoặc một khoảng dừng cố ý đặt sau lúc giải sẽ tiêu hết thời hạn của token. Thay vào đó, hãy đặt mọi việc điều tiết trước lúc giải.
- Coi mọi lỗi như nhau. Timeout phía bạn, token bị từ chối và lỗi phía website cần xử lý khác nhau. Hãy ghi log riêng từng loại.
Ghi log đủ để chẩn đoán
Khi một lần submit thất bại, hãy ghi thời điểm token được cấp, thời điểm form được gửi, mã HTTP và vài trăm ký tự đầu của phản hồi. Với bốn giá trị này, bạn có thể thấy ngay token đã hết hạn hay website từ chối vì lý do khác. Đừng ghi toàn bộ token vào log dùng chung, vì một token hợp lệ là thông tin xác thực cho đúng thử thách đó.
Kiểm tra gì khi vẫn thất bại
- In ra khoảng thời gian giữa lúc token được trả về và lúc gửi form.
- Xác nhận
site_urlkhớp với trang đang hiển thị captcha. - Xác nhận sitekey được đọc từ trang hiện tại, không phải từ một bản sao cũ.
- Xác nhận tên trường form. Token PopularCaptcha đi vào
h-captcha-response, và thường cảg-recaptcha-response.
Để xem đầy đủ các phản hồi lỗi và cách xử lý từng loại, xem xử lý lỗi bộ giải captcha.