Tích hợp Cloudflare Turnstile trong PHP: Một hàm xác thực dùng ngay

Bạn muốn chặn bot đăng ký nhưng không dùng WordPress hay framework? Bài viết này cung cấp một hàm PHP thuần để xác thực Cloudflare Turnstile kèm ví dụ form HTML + JS. Chỉ cần dán mã, thay khóa site key/secret key là bạn có thể bảo vệ endpoint đăng ký, liên hệ, bình luận…

Tích hợp Cloudflare Turnstile trong PHP: Một hàm xác thực dùng ngay

Tổng quan luồng hoạt động

  1. Trang HTML nhúng widget Turnstile bằng data-sitekey.
  2. Khi người dùng submit, trình duyệt gửi token (trường cf-turnstile-response) về server.
  3. Server gọi POST https://challenges.cloudflare.com/turnstile/v0/siteverify với secret, response, và (tùy chọn) remoteip.
  4. Nếu trả về success=true thì tiếp tục xử lý (tạo tài khoản, gửi mail…).

Frontend: HTML + JS nhúng Turnstile

<!-- Thay YOUR_TURNSTILE_SITE_KEY bằng site key của bạn -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<form id="register-form" method="post" action="/register.php">
  <input type="text" name="username" placeholder="Tên đăng nhập" required>
  <input type="email" name="email" placeholder="Email" required>
  <input type="password" name="password" placeholder="Mật khẩu" minlength="8" required>

  <div class="cf-turnstile"
       data-sitekey="YOUR_TURNSTILE_SITE_KEY"
       data-theme="auto"
       data-callback="onTsOk"></div>

  <button type="submit" id="btn-submit" disabled>Đăng ký</button>
</form>

<script>
  // Khi token hợp lệ, Turnstile gọi callback này
  function onTsOk() {
    var btn = document.getElementById('btn-submit');
    if (btn) btn.disabled = false;
  }
</script>

Lưu ý: Turnstile tự chèn input ẩn tên cf-turnstile-response. Bạn không cần tự thêm trường này.

Backend: hàm PHP xác thực Turnstile (dùng được ở mọi dự án)

<?php
/**
 * Xác thực token Cloudflare Turnstile.
 * @param string $token   Giá trị từ trường POST 'cf-turnstile-response'
 * @param string $secret  Secret key của bạn (KHÔNG để lộ ra client)
 * @param string $remoteIp (tùy chọn) IP của client để tăng độ tin cậy
 * @param float  $minScore (tùy chọn) ngưỡng điểm cho các cấu hình có score (nếu có)
 * @return array [ 'ok' => bool, 'reason' => string, 'raw' => array ]
 */
function verify_turnstile(string $token, string $secret, string $remoteIp = '', float $minScore = 0.0): array
{
    if ($token === '') {
        return ['ok' => false, 'reason' => 'missing_token', 'raw' => []];
    }
    if ($secret === '') {
        return ['ok' => false, 'reason' => 'missing_secret', 'raw' => []];
    }

    $endpoint = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
    $postData = [
        'secret'   => $secret,
        'response' => $token,
    ];
    if ($remoteIp !== '') {
        $postData['remoteip'] = $remoteIp;
    }

    $ch = curl_init($endpoint);
    curl_setopt_array($ch, [
        CURLOPT_POST            => true,
        CURLOPT_RETURNTRANSFER  => true,
        CURLOPT_HTTPHEADER      => ['Content-Type: application/x-www-form-urlencoded'],
        CURLOPT_POSTFIELDS      => http_build_query($postData, '', '&'),
        CURLOPT_TIMEOUT         => 10,
        CURLOPT_SSL_VERIFYPEER  => true,
        CURLOPT_SSL_VERIFYHOST  => 2,
    ]);

    $respBody = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlErr  = curl_error($ch);
    curl_close($ch);

    if ($respBody === false) {
        return ['ok' => false, 'reason' => 'transport_error:' . $curlErr, 'raw' => []];
    }
    if ($httpCode !== 200) {
        return ['ok' => false, 'reason' => 'bad_http:' . $httpCode, 'raw' => []];
    }

    $data = json_decode($respBody, true) ?: [];
    // $data ví dụ: ['success' => true, 'challenge_ts' => '...', 'hostname' => '...', 'action' => '...', 'cdata' => '...', 'error-codes' => []]

    if (empty($data['success'])) {
        $reason = 'invalid_token';
        if (!empty($data['error-codes']) && is_array($data['error-codes'])) {
            $reason .= ':' . implode(',', $data['error-codes']);
        }
        return ['ok' => false, 'reason' => $reason, 'raw' => $data];
    }

    // Nếu bạn cấu hình action/cdata ở client, có thể kiểm tra khớp:
    // if (($data['action'] ?? '') !== 'register') return ['ok' => false, 'reason' => 'action_mismatch', 'raw' => $data];

    // Một số cấu hình có score (không phải lúc nào cũng có). Nếu có thì kiểm tra ngưỡng.
    if ($minScore > 0 && isset($data['score']) && is_numeric($data['score']) && (float)$data['score'] < $minScore) {
        return ['ok' => false, 'reason' => 'low_score:' . $data['score'], 'raw' => $data];
    }

    return ['ok' => true, 'reason' => 'ok', 'raw' => $data];
}

Ví dụ xử lý đăng ký đơn giản với hàm trên

<?php
// register.php
// Thay YOUR_TURNSTILE_SECRET_KEY bằng secret key thực của bạn (đặt qua biến môi trường càng tốt)
$TURNSTILE_SECRET = getenv('TURNSTILE_SECRET') ?: 'YOUR_TURNSTILE_SECRET_KEY';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $email    = trim($_POST['email'] ?? '');
    $password = (string)($_POST['password'] ?? '');
    $token    = (string)($_POST['cf-turnstile-response'] ?? '');
    $ip       = $_SERVER['REMOTE_ADDR'] ?? '';

    // 1) Kiểm tra đầu vào cơ bản
    $errors = [];
    if ($username === '' || !preg_match('/^[A-Za-z0-9._-]{3,32}$/', $username)) {
        $errors[] = 'Tên đăng nhập không hợp lệ.';
    }
    if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = 'Email không hợp lệ.';
    }
    if (strlen($password) < 8) {
        $errors[] = 'Mật khẩu tối thiểu 8 ký tự.';
    }

    // 2) Kiểm tra Turnstile
    require __DIR__ . '/turnstile.php'; // file chứa hàm verify_turnstile ở trên (hoặc dán trực tiếp)
    $ts = verify_turnstile($token, $TURNSTILE_SECRET, $ip, 0.0); // minScore=0.0 (tùy chỉnh)
    if (!$ts['ok']) {
        $errors[] = 'Xác minh Turnstile thất bại: ' . $ts['reason'];
    }

    // 3) Nếu không lỗi thì tiếp tục đăng ký (ví dụ chèn DB giả lập)
    if (!$errors) {
        // TODO: Kiểm tra trùng username/email, băm mật khẩu, lưu DB, gửi email kích hoạt...
        // password_hash() để băm mật khẩu:
        // $hash = password_hash($password, PASSWORD_DEFAULT);

        // Ví dụ phản hồi thành công
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['status' => 'ok', 'message' => 'Tạo tài khoản thành công']);
        exit;
    }

    // 4) Có lỗi: trả JSON lỗi
    header('Content-Type: application/json; charset=utf-8', true, 400);
    echo json_encode(['status' => 'error', 'errors' => $errors]);
    exit;
}
?>

Kiểm thử nhanh

  • Chạy một web server cục bộ (vd: php -S localhost:8000) và truy cập trang form.
  • Quan sát DevTools → Network để thấy trường cf-turnstile-response được gửi.
  • Tạm in ra $ts['raw'] khi ở môi trường dev để xem chi tiết phản hồi và error-codes.

Thực hành tốt nhất

  • Giữ secret key ở biến môi trường, không commit vào repo.
  • Kết hợp thêm rate limit theo IP/email và một trường honeypot ẩn để tăng lớp bảo vệ.
  • Không cache nội dung chứa token theo người dùng; nếu có CDN, cấu hình bỏ qua cache cho route xử lý POST.
  • Nếu dùng chế độ có action/cdata, hãy kiểm tra khớp ở server để tránh lạm dụng token chéo form.

Câu hỏi thường gặp

Token có hết hạn không? Có. Token Turnstile sống ngắn; đừng tái sử dụng. Nếu form mở lâu, hãy cho phép reset widget khi lỗi.

Có cần gửi remote IP? Không bắt buộc, nhưng có thể giúp tăng độ chính xác đánh giá rủi ro.

Vì sao nhận lỗi transport/bad_http? Thường do firewall, DNS, hoặc server không ra ngoài. Kiểm tra cURL, DNS, cổng 443 và chứng chỉ SSL.

Kết luận

Chỉ với một hàm PHP nhỏ gọn và vài dòng HTML, bạn đã có lớp phòng thủ chống spam gọn nhẹ, không làm phiền người dùng. Từ ví dụ trên, bạn có thể tái sử dụng cho mọi endpoint nhận form mà không cần phụ thuộc vào nền tảng cụ thể.

Bình luận


  • Không có bình luận.

Công cụ trực tuyến

Nhấn Ctrl + \ trên máy tính, hoặc vuốt sang trái ở bất kỳ đâu trên mobile.

Đăng nhập





Đang tải...