🤖 Captcha và chống bot cơ bản

By Better Wise

Bài trước bạn đã dựng quota theo user và chặn request rác ngay từ khâu validate input. Nhưng quota vẫn cho bot tiêu hết hạn mức của chính nó, và validate vẫn cho qua nếu payload trông hợp lệ. Bài này là lớp cuối: phân biệt người thật với bot, và chỉ mở cửa cho người thật.

Sau 15 phút này, bạn sẽ:

  • Nhận ra 4 dấu hiệu bot rẻ tiền, không cần captcha
  • Biết đặt captcha ở endpoint nào và ở tầng nào trong chuỗi bảo vệ
  • Verify token captcha đúng cách ở server
  • Tránh chặn oan người dùng thật (bẫy tôi đã mắc và mất tiền vì nó)

Vì sao một script 200 dòng có thể đốt 80 USD của bạn

Chuyện thật của tôi: endpoint /api/chat gọi model bên thứ ba, mỗi request tốn khoảng 0,002 USD. Rate limit theo IP có, quota theo user có. Nhưng bot đăng ký 40.000 tài khoản bằng email tạm, mỗi tài khoản một request. Sáng hôm sau tôi có hóa đơn khoảng 80 USD và một dashboard đầy user rác.

Bài học: rate limiting và quota giới hạn tốc độ và hạn mức, không giới hạn việc ai đang gọi. Captcha và các dấu hiệu bot trả lời câu hỏi "ai".

Dấu hiệu bot nhìn thấy mà không cần captcha

Trước khi bắt người dùng giải captcha, hãy thử lọc bằng những thứ miễn phí. Bạn đã có request trong tay rồi.

Dấu hiệu Cách kiểm tra Ghi chú
Honeypot field Form có field ẩn, người thật không thấy nên không điền Bot điền hết mọi field, gần như chắc chắn dính
Thời gian submit So timestamp lúc render form và lúc submit Người thật mất vài giây, bot thường dưới 1 giây
Header thiếu User-Agent trống hoặc giả, không có Accept-Language Đừng chặn tuyệt đối, chỉ cộng điểm rủi ro
Nhịp đều như đồng hồ Khoảng cách giữa các request gần như bằng nhau Người thật có jitter tự nhiên

Cách dùng: mỗi dấu hiệu là một điểm rủi ro, không phải một bản án. Cộng điểm lại rồi mới quyết định.

flowchart TD
    Req["📥<br/>Request vào"] --> RL{"🚦<br/>Rate limit và quota OK?"}
    RL -- Không --> Block1["⛔ 429"]
    RL -- Có --> Sig{"🕵️<br/>Điểm rủi ro bot"}
    Sig -- "Cao" --> Chal["🧩<br/>Yêu cầu captcha"]
    Sig -- "Thấp" --> Biz["⚙️<br/>Xử lý và gọi API AI"]
    Chal --> Ver{"✅<br/>Verify token ở server"}
    Ver -- Sai --> Block2["⛔ 403"]
    Ver -- Đúng --> Biz

Bài tập 1 (cho project của bạn): endpoint /api/chat sắp mở. Liệt kê 3 dấu hiệu rủi ro bạn sẽ log cho nó, và ngưỡng điểm để bật captcha.

Xem gợi ý

Ba dấu hiệu hợp lý: honeypot trên form đăng ký có giá trị, thời gian từ lúc tạo session đến request đầu tiên dưới 1 giây, User-Agent trống. Ngưỡng ví dụ: từ 2 điểm trở lên thì yêu cầu captcha, dưới 2 điểm thì xử lý bình thường. Điều quan trọng là bạn ghi log điểm này để sau đó soát lại ai bị chặn.

Gắn captcha đúng chỗ, đúng liều

Captcha là lớp cuối, không phải lớp đầu. Nếu bạn bắt mọi request phải giải captcha, bạn đã tự làm chậm sản phẩm và tăng tỉ lệ bỏ đi của người dùng thật.

Ba câu hỏi để chọn chỗ đặt:

  1. Endpoint này có tốn tiền hoặc tạo tài nguyên không? Đăng ký, gửi form liên hệ, /api/chat, mọi endpoint gọi API AI bên thứ ba: có. GET /products: không.
  2. Có bị lạm dụng theo kiểu hàng loạt không? Nếu một user thật chỉ gọi vài lần mỗi ngày mà bot gọi hàng nghìn lần, đây là chỗ đặt.
  3. Có cách rẻ hơn để lọc trước không? Honeypot và kiểm tra timing luôn rẻ hơn captcha. Dùng trước.

Về loại captcha:

  • Visible challenge: hiện ô "tôi không phải robot". Rẻ, dễ hiểu, nhưng làm phiền mọi người.
  • Invisible / score-based: chấm điểm ngầm, chỉ hiện thử thách khi điểm thấp. Trải nghiệm tốt hơn, đây là lựa chọn mặc định của tôi.
  • Proof-of-work nhẹ: client phải tính một việc nhỏ trước khi gửi. Không phụ thuộc bên thứ ba, nhưng vẫn cần code client.

Dù chọn loại nào, phần verify phải nằm ở server:

  • Gửi token lên server, gọi API của provider để xác thực, không tin biến isVerified do client gửi.
  • Token chỉ dùng một lần, gắn với action và có thời hạn ngắn (khoảng 2 phút).
  • Kiểm tra hostname hoặc action trong response để chống dùng lại token từ site khác.

Bài tập 2: đồng nghiệp viết if (req.body.captchaOk) { cho qua }. Nêu 2 lý do phải sửa.

Xem gợi ý

Thứ nhất, client tự khai thì bot chỉ cần gửi captchaOk: true. Thứ hai, kể cả khi client gọi provider thật, token vẫn có thể bị dùng lại hoặc bị lấy từ phiên khác nếu server không verify với provider và không ràng buộc action, thời hạn.

Giữ người thật ở lại: chống bot mà không chặn oan

Chặn oan người thật tốn hơn để bot lọt. Bot lọt thì đã có quota chặn thiệt hại; người thật bị chặn thì bạn mất user trả tiền và mất luôn niềm tin.

Vài cách giữ cân bằng:

  • Progressive challenge: 3 request đầu không thử thách, chỉ bật captcha khi điểm rủi ro cao hoặc tần suất bất thường.
  • Allowlist nội bộ: team bạn, monitoring, app mobile dùng token riêng. Đừng để chính bạn bị chặn lúc 2 giờ sáng.
  • Fail closed với endpoint tốn tiền: nếu provider captcha chết, từ chối request và trả lỗi rõ ràng. Với endpoint đọc miễn phí thì có thể fail open.
  • Thông báo tử tế: "Vui lòng xác nhận bạn không phải robot" kèm link hỗ trợ, đừng trả về trang trắng.
  • Đừng quên accessibility: luôn có phương án thay thế cho người không giải được hình ảnh.

Về chi phí: captcha thường có free tier, sau đó khoảng 1 USD cho 1.000 lượt giải. Con số này nhỏ so với 80 USD bot đã đốt, nhưng hãy tính cả tỉ lệ người dùng bỏ đi khi bị hỏi quá nhiều.

Ba lỗi tôi đã mắc khi lắp captcha

  1. Đặt captcha lên mọi request. Trang chủ chậm, người dùng phàn nàn, mà bot vẫn lọt vì tôi quên verify token ở server. Captcha chỉ có giá trị khi đặt đúng chỗ và kiểm tra đúng phía.
  2. Không log lý do chặn. Khi khách báo "tôi không vào được", tôi không biết họ bị chặn vì honeypot hay vì điểm rủi ro. Luôn log reason và risk_score cạnh mỗi lần chặn.
  3. Để fail open cho endpoint gọi AI. Provider captcha downtime 30 phút, tôi cho qua hết, và đúng 30 phút đó bot quét sạch quota. Với endpoint tốn tiền, hãy chọn fail closed.

Quy tắc của tôi: đặt giới hạn trước khi mở cửa. Rate limit, quota, validate, rồi mới tới captcha. Và luôn có một đường lui cho người thật.

Chốt lại và bước tiếp theo

Bốn điều mang đi:

  • Bot khác người thật ở honeypot, timing, header và nhịp request. Lọc bằng điểm rủi ro trước khi bắt ai giải captcha.
  • Captcha đặt ở lớp cuối, chỉ ở endpoint tốn tiền hoặc tạo tài nguyên, ưu tiên invisible.
  • Token phải verify ở server, dùng một lần, có thời hạn ngắn.
  • Ưu tiên không chặn oan, log lý do chặn, và fail closed với endpoint gọi API AI.

Bài tiếp theo là checkpoint: bạn sẽ ghép rate limiting, quota, validate và captcha thành một chuỗi duy nhất cho project, kèm bài tập tình huống để kiểm tra lớp nào đang thủng.

Captcha nên được đặt ở _____ trong chuỗi bảo vệ, sau rate limiting, quota và validate input. Honeypot là _____; nếu field này có giá trị thì gần như chắc chắn là bot. Token captcha phải được verify ở _____, chỉ dùng _____ và có thời hạn ngắn. Với endpoint tốn tiền, khi captcha provider gặp sự cố nên chọn hướng _____ thay vì cho qua tất cả.

Front: Captcha nên nằm ở đâu trong chuỗi bảo vệ? Back: Lớp cuối, sau rate limiting, quota và validate input.

Front: Honeypot là gì? Back: Một field ẩn trong form mà người thật không thấy nên không điền, còn bot thì điền.

Front: Vì sao kiểm tra thời gian submit phát hiện được bot? Back: Bot thường submit quá nhanh, dưới 1 đến 2 giây sau khi form được render.

Front: Token captcha phải được verify ở đâu? Back: Ở server, bằng cách gọi API của provider, không tin cờ do client gửi.

Front: Token captcha nên dùng bao nhiêu lần? Back: Một lần, gắn với action cụ thể và có thời hạn ngắn, ví dụ 2 phút.

Front: Endpoint nào nên gắn captcha? Back: Endpoint tốn tiền hoặc tạo tài nguyên: đăng ký, gửi form, /api/chat, mọi endpoint gọi API AI.

Front: Vì sao không nên gắn captcha lên mọi request GET? Back: GET thường rẻ, gắn captcha chỉ làm chậm người thật và tăng tỉ lệ bỏ đi.

Front: Invisible captcha khác visible captcha thế nào? Back: Invisible chấm điểm rủi ro ngầm và chỉ hiện thử thách khi điểm thấp; visible luôn hỏi người dùng.

Front: Khi captcha provider gặp sự cố thì xử lý thế nào? Back: Endpoint tốn tiền thì fail closed (từ chối), endpoint đọc miễn phí có thể fail open.

Front: Dấu hiệu bot rẻ tiền từ header là gì? Back: User-Agent trống hoặc giả, thiếu các header trình duyệt thường có như Accept-Language.

Front: Vì sao bot traffic lại đắt với API AI? Back: Mỗi request đều tiêu token và bị tính tiền theo lượng dùng, nên số request tăng là chi phí tăng.

Front: Vì sao chặn oan người thật tốn hơn để bot lọt? Back: Bot lọt đã bị quota giới hạn thiệt hại, còn người thật bị chặn thì mất user và mất niềm tin.

Front: Nên dùng gì để lọc bot trước khi bật captcha? Back: Điểm rủi ro cộng dồn từ honeypot, timing, header và nhịp request.