Cùng một script Playwright: chạy trên laptop thì qua êm, đẩy lên server CI là dính thử thách ảnh liên tục. Nguyên nhân gần như luôn là điểm reCAPTCHA v3 — số thực từ 0,0 (chắc chắn là bot) đến 1,0 (chắc chắn là người) do bộ máy Advanced Risk Analysis (ARA) của Google chấm cho từng request.
Ba thứ kéo điểm xuống mạnh nhất, xếp theo đúng trọng số:
- Không có tín hiệu hành vi nào — không chuột, không cuộn, không nhịp gõ phím.
- Trình duyệt chạy chế độ headless, để lộ đặc trưng của môi trường máy chủ.
- IP nằm trong dải trung tâm dữ liệu.
Google không công bố thuật toán, nhưng thử nghiệm cộng đồng đã khoanh vùng khá rõ nhóm nào nặng ký. Bài này mổ xẻ từng nhóm kèm biên độ ước lượng, và chỉ ra khi nào việc "chỉnh cho đẹp điểm" không còn đáng công so với lấy token qua API.
Điểm cuối cùng là tổng hợp của năm nhóm tín hiệu
reCAPTCHA chấm điểm dựa trên năm nhóm tín hiệu, mỗi nhóm đóng góp một phần vào kết quả:
Final Score = f(Browser Signals, Behavioral Signals, Network Signals,
History Signals, Environmental Signals)
Bài viết đi lần lượt qua năm nhóm, nhưng khi debug thì nên theo trọng số:
- Hành vi — nặng nhất, xem trước tiên.
- Mạng — loại IP và fingerprint TLS.
- Trình duyệt — headless lộ mình ở đây.
- Môi trường và lịch sử — chỉ khuếch đại nghi ngờ có sẵn.
Các con số ± trong bài là biên độ quan sát từ thử nghiệm công khai, không phải hằng số Google công bố. Đọc chúng như thứ tự ưu tiên, không phải công thức cộng trừ.
Nhóm 1: tín hiệu trình duyệt (trọng số cao)
Nhóm này trả lời một câu duy nhất: client đang gọi là trình duyệt thật hay công cụ tự động?
Những API JavaScript bị soi đầu tiên
reCAPTCHA đọc các API mà trình duyệt headless thường thiếu hoặc trả về khác:
| API được kiểm tra | Giá trị ở trình duyệt thật | Dấu hiệu headless |
|---|---|---|
navigator.webdriver |
undefined hoặc false |
true (Selenium, Puppeteer) |
navigator.plugins |
PluginArray có từ 1 mục | Rỗng hoặc thiếu |
navigator.languages |
Mảng chuỗi locale | Rỗng hoặc thiếu |
navigator.hardwareConcurrency |
2–16 (khớp số nhân CPU) | Đôi khi 0 hoặc undefined |
navigator.deviceMemory |
2–16 GB | Thiếu ở một số bản headless |
window.chrome |
Object có runtime | Thiếu trong Chrome headless |
Notification.permission |
"default", "granted", "denied" | Ném lỗi hoặc không tồn tại |
Fingerprint canvas
reCAPTCHA vẽ ẩn một canvas rồi đọc lại dữ liệu pixel. Kết quả là chuỗi đặc trưng cho tổ hợp trình duyệt/OS/GPU của máy đang chạy.
// reCAPTCHA performs something similar to:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = "top";
ctx.font = "14px 'Arial'";
ctx.fillText("Hello", 2, 2);
const fingerprint = canvas.toDataURL();
| Kết quả canvas đọc được | Ảnh hưởng tới điểm |
|---|---|
| Ổn định, khớp với trình duyệt/OS mà client tự khai | Trung tính |
| Trùng mẫu headless đã biết | -0,3 đến -0,5 |
| Bị chặn hoặc trả về dữ liệu đồng nhất | -0,4 đến -0,6 |
Chuỗi renderer WebGL
Chuỗi renderer của WebGL lộ ra GPU đang dựng hình:
const gl = document.createElement('canvas').getContext('webgl');
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
// Expected: "ANGLE (NVIDIA GeForce GTX 1060...)"
// Headless: "SwiftShader" or "Google SwiftShader"
SwiftShader là renderer phần mềm mà Chrome headless dùng khi máy không có GPU, và nó là chỉ dấu bot khá mạnh. Đây cũng là lý do container CI không gắn GPU gần như luôn cho điểm thấp hơn máy dev.
Nhóm 2: tín hiệu hành vi (trọng số rất cao)
Nhóm nặng ký nhất, và mức phạt lớn nhất cả bài nằm ở đây: phiên không có một chuyển động chuột nào. Việc thu thập bắt đầu ngay từ lúc trang tải xong, không đợi bạn bấm nút gửi.
Chuyển động chuột
reCAPTCHA ghi lại tọa độ chuột kèm dấu thời gian trong suốt phiên xem trang:
| Tín hiệu | Mẫu của người thật | Mẫu của bot |
|---|---|---|
| Số sự kiện di chuyển | 50–500+ mỗi lượt xem trang | 0–5 sự kiện |
| Quỹ đạo | Đường cong, tốc độ thay đổi | Đường thẳng, tốc độ đều |
| Gia tốc | Tăng/giảm tốc tự nhiên | Khởi động và dừng tức thì |
| Rung vi mô | Lệch nhẹ khi giữ chuột một chỗ | Đứng yên tuyệt đối |
| Vượt mục tiêu | Chệch rồi chỉnh lại | Trỏ trúng ngay lần đầu |
Nhịp gõ phím
Inter-key timing:
Human: Variable (50-300ms between keys, varies by key pair)
Bot: Constant (all keys at same interval, or instant paste)
Key events:
Human: keydown → keypress → keyup for each character
Bot: May skip keypress, or fire all events simultaneously
Corrections:
Human: Occasional backspace, re-typing
Bot: Perfect input without corrections
Điền tức thì — dán từ clipboard hoặc gán thẳng value bằng JavaScript — luôn cho điểm thấp hơn gõ từng ký tự.
Cuộn trang và thứ tự chạm vào các phần tử
Hai tín hiệu này đi cùng nhau: reCAPTCHA vừa xem bạn có cuộn không, vừa xem bạn chạm vào phần tử theo thứ tự nào.
| Tín hiệu | Mẫu của người thật | Mẫu của bot |
|---|---|---|
| Cuộn trang | Có quán tính, tốc độ thay đổi, dừng lại để đọc | Không cuộn, hoặc nhảy thẳng tới form |
| Thứ tự chạm | Bấm loanh quanh, rê chuột qua link, đọc rồi mới điền | Vào thẳng field đầu, điền tuần tự, submit |
Nhóm 3: tín hiệu môi trường (trọng số thấp–trung bình)
Nhóm này nhẹ ký hơn hẳn, nhưng mâu thuẫn phát hiện ở đây sẽ khuếch đại nghi ngờ từ các nhóm trên.
Múi giờ có khớp với IP không
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Should match IP geolocation
// "America/New_York" with a New York IP → consistent
// "America/New_York" with a Tokyo IP → suspicious
Trường hợp hay gặp với đội Việt Nam: container CI để mặc định UTC, IP ra ngoài là VPS Singapore, còn Accept-Language khai vi-VN. Ba mảnh ghép lệch nhau chính là thứ ARA tìm.
Kích thước cửa sổ và trạng thái phần cứng
Hai giá trị còn lại đọc thẳng từ trình duyệt: kích thước màn hình/cửa sổ và, nếu hỗ trợ, trạng thái pin.
screen.width, screen.height // Physical screen
window.innerWidth, innerHeight // Browser window
window.outerWidth, outerHeight // Including browser chrome
navigator.getBattery().then(battery => {
battery.level; // 0.0 to 1.0
battery.charging; // true/false
});
Kích thước bất thường — 0×0, quá lớn, hoặc đúng bằng mặc định của công cụ — là thứ đáng sửa. Battery API thì chỉ là tín hiệu phụ, đừng mất thời gian cho nó.
Nhóm 4: tín hiệu mạng (trọng số trung bình–cao)
Danh tiếng IP
Google duy trì cơ sở dữ liệu danh tiếng IP, dựng từ:
- Kết quả các thử thách reCAPTCHA trước đó từ IP này
- Dải IP trung tâm dữ liệu đã biết (AWS, GCP, Azure, DigitalOcean…)
- Dải IP của nhà cung cấp proxy/VPN đã biết
- Báo cáo lạm dụng, cơ sở dữ liệu spam và tín hiệu gian lận từ quảng cáo
| Loại IP | Ảnh hưởng điểm điển hình |
|---|---|
| Dân dụng, ít lạm dụng | Trung tính đến +0,1 |
| Dân dụng, có lạm dụng | -0,1 đến -0,2 |
| Trung tâm dữ liệu | -0,3 đến -0,5 |
| Proxy/VPN đã biết | -0,2 đến -0,4 |
| Node thoát Tor | -0,4 đến -0,6 |
| Bị gắn cờ lạm dụng gần đây | -0,3 đến -0,6 |
Fingerprint TLS (JA3/JA4)
Quá trình bắt tay TLS tiết lộ client hỗ trợ bộ mật mã và extension nào. Mỗi runtime có một chữ ký riêng:
| Client | Chữ ký để lại |
|---|---|
| Chrome | Thứ tự cipher đặc trưng, TLS 1.3, có extension GREASE |
| Python requests | Thứ tự cipher khác, không GREASE, ít extension hơn |
| curl | Rất ít extension, mang dấu vết OpenSSL |
Đây là lý do request bằng requests thuần, dù điền đủ header, vẫn khó đạt điểm cao: chữ ký TLS đã lộ trước khi JavaScript kịp chạy.
Header HTTP
Máy chủ reCAPTCHA đọc header của chính request gọi tới:
| Header | Dấu hiệu người thật | Dấu hiệu bot |
|---|---|---|
User-Agent |
Khớp một bản phát hành trình duyệt có thật | Quá cũ, hoặc UA của công cụ tự động |
Accept-Language |
Có danh sách locale cụ thể | Thiếu hoặc chung chung |
Accept-Encoding |
gzip, deflate, br |
Thiếu hoặc bất thường |
Sec-CH-UA |
Client hints khớp với trình duyệt khai báo | Thiếu (kiểu header trước 2022) |
Nhóm 5: tín hiệu lịch sử (trọng số trung bình)
Cookie Google và cookie của chính reCAPTCHA
Cả hai loại cookie đều trả lời một câu: phiên này có lịch sử hay không?
| Cookie đọc được | Ý nghĩa với ARA |
|---|---|
SID, HSID, SSID |
Phiên Google đang đăng nhập |
| Không có cookie Google | Người dùng lạ, điểm khởi đầu thấp hơn |
_GRECAPTCHA |
Đã từng giải thành công trước đó |
rc::a, rc::b, rc::c |
Dữ liệu phân tích rủi ro của reCAPTCHA |
Mỗi lần test khởi tạo profile trắng là mỗi lần bạn tự bỏ đi phần điểm mà một phiên có lịch sử được cộng.
Danh tiếng liên trang
Google theo dõi hành vi reCAPTCHA trên mọi site dùng chung một profile: điểm tốt ở site A nâng điểm cho site B, và ngược lại.
Bảng tổng hợp: tín hiệu nào lấy đi bao nhiêu điểm
Toàn bộ biên độ ước lượng của năm nhóm, xếp từ nặng đến nhẹ:
| Tín hiệu | Ảnh hưởng ước tính |
|---|---|
| Không có chuyển động chuột | -0,4 đến -0,7 |
| Canvas bị chặn hoặc trả dữ liệu đồng nhất | -0,4 đến -0,6 |
| Tor exit node hoặc IP mới bị gắn cờ | -0,3 đến -0,6 |
| IP trung tâm dữ liệu | -0,3 đến -0,5 |
| Renderer SwiftShader (headless không GPU) | -0,3 đến -0,5 |
| API JavaScript thiếu hoặc sai lệch | -0,3 đến -0,5 |
| Fingerprint TLS không phải trình duyệt | -0,2 đến -0,4 |
| Không cuộn, đi thẳng vào form | -0,1 đến -0,3 |
| Kích thước cửa sổ bất thường | -0,1 đến -0,2 |
| Profile trắng, sạch cookie | -0,1 đến -0,2 |
| Điền field tức thì thay vì gõ | -0,1 đến -0,2 |
| Phiên Google đang đăng nhập | +0,1 đến +0,3 |
Một tình huống quen thuộc ở TP.HCM
Một đội QA thuê ngoài chạy test hồi quy cho luồng đăng nhập của khách hàng. Trên máy dev ở văn phòng, mọi thứ đều xanh. Đưa lên GitLab Runner trong một VPS, tỷ lệ pass rơi thẳng đứng.
Đối chiếu với bảng trên là ra ngay lý do:
- Chrome headless không GPU → renderer SwiftShader
- IP của VPS → dải trung tâm dữ liệu
- Profile mới mỗi lần chạy → sạch cookie
- Kịch bản đi thẳng vào field
username200 ms sau khi trang tải xong → không chuột, không cuộn
Không tín hiệu nào là "lỗi" cả; chỉ là tổ hợp đó đọc lên giống bot. Cùng logic ấy áp cho các đội theo dõi giá trên Shopee, Lazada hay Tiki: job chạy đêm từ một dải IP cố định sẽ tụt điểm dần theo tuần, kể cả khi code không đổi dòng nào. Nếu dữ liệu thu về có kèm thông tin cá nhân, hãy giữ log truy cập và tối thiểu hóa dữ liệu lưu trữ theo Nghị định 13/2023/NĐ-CP — lưu ý vận hành, không phải tư vấn pháp lý.
Cải thiện điểm: cái gì đáng làm, cái gì không
| Hướng xử lý | Mức cải thiện ước tính | Độ khó |
|---|---|---|
| Chrome có giao diện thay vì headless | +0,3 đến +0,5 | Thấp |
| Cho script test cuộn và rê chuột như người dùng | +0,2 đến +0,4 | Trung bình |
| Đa dạng nguồn IP thay vì một dải cố định | +0,2 đến +0,3 | Thấp (tốn chi phí) |
| Giữ cookie phiên QA giữa các lần chạy | +0,1 đến +0,2 | Trung bình |
Điền đủ User-Agent, Accept-Language, client hints |
+0,1 đến +0,2 | Thấp |
| Khớp fingerprint TLS với Chrome | +0,1 đến +0,3 | Cao |
Vấn đề của cả cột bên trái: đó là việc bảo trì không có điểm dừng. Google cập nhật mô hình, bạn cập nhật theo. Với pipeline chạy hằng ngày, chi phí thời gian đó thường lớn hơn chi phí gọi API.
Hoặc: lấy token qua API CaptchaAI
Thay vì tinh chỉnh từng tín hiệu, luồng chỉ còn ba bước:
- Gửi task tới
in.phpkèmgooglekey,pageurl,versionvàaction. - Polling
res.phpbằng ID task cho tới khi có kết quả. - Đặt token trả về vào field
g-recaptcha-responserồi submit như bình thường.
import requests
import time
API_KEY = "YOUR_API_KEY"
# Request a high-score token
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": "6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
"pageurl": "https://staging.example.com/qa-login",
"version": "v3",
"action": "login",
"json": 1,
})
task_id = submit.json()["request"]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}).json()
if result.get("status") == 1:
token = result["request"]
print(f"High-score token received")
break
Vòng lặp trên gọi res.php mỗi 5 giây cho tới khi có kết quả (polling — chủ động hỏi kết quả định kỳ). Môi trường giải phía CaptchaAI đã xử lý sẵn tín hiệu trình duyệt, hành vi và mạng, nên bạn không phải dựng lại hạ tầng đó trong runner. Chi phí tính theo thread — số CAPTCHA giải song song — chứ không theo từng lần giải: gói BASIC ($15/tháng, 5 thread) là mức thấp nhất, niêm yết bằng USD.
Câu hỏi thường gặp
Điểm reCAPTCHA v3 bao nhiêu là đủ để qua?
Phần lớn site đặt ngưỡng 0,5–0,7; từ 0,7 trở lên thường được coi là giống người. Ngưỡng do chủ site tự chọn, nên không có con số "an toàn" chung.
Chạy headless trên VPS thì có bao giờ đạt điểm cao không?
Rất khó: renderer SwiftShader, IP trung tâm dữ liệu và profile trắng là ba mức phạt nặng cộng dồn. Chrome có giao diện trên máy thật khá hơn, nhưng ở quy mô chạy hằng ngày thì lấy token qua API đỡ công bảo trì hơn.
CaptchaAI có tham số để chỉ định điểm mong muốn không?
Không. Điểm do Google quyết định, API không có tham số kiểu score-target. Cái bạn kiểm soát được là các trường tùy chọn cookies, userAgent và proxy gửi kèm request.
Điểm thấp thì nên sửa hạ tầng hay chuyển sang API?
Vài chục lần mỗi tuần trên máy dev thì chỉnh lại môi trường là đủ. Với pipeline chạy liên tục hoặc job thu thập dữ liệu định kỳ, tự bảo trì tín hiệu tốn công hơn gọi API.
Ngoài reCAPTCHA v3, CaptchaAI còn giải được những loại nào?
reCAPTCHA v2 và v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, CAPTCHA ảnh/OCR, CAPTCHA dạng lưới và BLS CAPTCHA. hCaptcha và FunCaptcha chưa được hỗ trợ; GeeTest v4 mới sắp ra mắt; CaptchaFox, Friendly Captcha và Lemin đang ở giai đoạn beta.
Tóm tắt
Điểm v3 là tổng hợp của fingerprint trình duyệt, hành vi, môi trường, danh tiếng mạng và lịch sử phiên. Khi debug, đi từ nhóm hành vi và nhóm mạng trước — đó là nơi mất điểm nhiều nhất. Còn nếu cần token ổn định cho automation chạy hằng ngày, hãy dùng CaptchaAI và gắn thêm cookies, userAgent cùng proxy vào request để reCAPTCHA đọc được bối cảnh đầy đủ hơn.