Hentikan spam formulir kontak

Hentikan spam formulir kontak6 menit baca

Cara menambahkan Cloudflare Turnstile ke formulir HTML

Tambahkan Cloudflare Turnstile ke formulir HTML biasa: widget, siteverify di sisi server, key uji, dan cara menjalankannya tanpa key atau server sendiri.

Preet SutharPendiri, Ikiform

Tambahkan skrip Turnstile dan elemen cf-turnstile di dalam formulir Anda; widget menaruh token di field tersembunyi cf-turnstile-response. Lalu kirim token itu dan secret key Anda ke endpoint siteverify Cloudflare dari server, dan terima kiriman hanya jika jawabannya success. Tanpa server, biarkan form backend Anda yang memverifikasinya, atau pakai pemeriksaan terkelola Ikiform, yang sama sekali tidak butuh key.

Apa yang berbeda dengan Turnstile

Turnstile punya dua bagian, dan formulir HTML biasa hanya punya tempat untuk satu. Widget berjalan di browser dengan site key publik Anda dan menghasilkan token. Pemeriksaan berjalan di server dengan secret key Anda, yang tidak boleh pernah sampai ke halaman. Formulir yang menampilkan widget tetapi tidak pernah memverifikasi token hanyalah hiasan: bot mengirim langsung ke endpoint Anda dan melewati widget sepenuhnya.

Jadi pertanyaan sebenarnya untuk formulir HTML adalah di mana secret disimpan. Halaman ini membahas tiga jawaban: server kecil milik Anda sendiri, form backend yang menyimpan key Anda, dan pemeriksaan terkelola di mana tak ada yang memegang key selain backend.

Perjalanan token Turnstile, dari widget hingga keputusanDi browser, dengan site keyDi server, dengan secret keywidget berjalanbiasanya takterlihattokenditerbitkaninput tersembunyicf-turnstile-responseformulir dikirimtoken ikut bersamafieldsiteverifysecret + token + IPpengunjungperiksa jawabansuccess, hostname,actionterima kirimantolakkirimsemua cocokada yang janggalPerjalanan token Turnstile, dari widget hingga keputusanDi browser, dengan site keyDi server, dengan secret keywidget berjalanbiasanya takterlihattokenditerbitkaninput tersembunyicf-turnstile-responseformulirdikirimtoken ikut bersamafieldsiteverifysecret + token + IPpengunjungperiksa jawabansuccess, hostname,actionterima kirimantolakkirimsemua cocokada yang janggal
  1. Widget Turnstile berjalan di browser dengan site key publik Anda. Kebanyakan pengunjung tidak pernah melihatnya melakukan apa pun.
  2. Widget menerbitkan token dan menaruhnya di input tersembunyi cf-turnstile-response di dalam formulir.
  3. Formulir dikirim, dan token ikut bersama field lainnya.
  4. Server Anda mengirim secret key, token, dan IP pengunjung ke endpoint siteverify Cloudflare.
  5. Server memeriksa bahwa jawabannya success dan hostname serta action sesuai yang Anda harapkan.
  6. Jika semuanya cocok, kiriman diterima. Jika ada yang janggal, kiriman ditolak.
Token berlaku 300 detik dan hanya bisa diverifikasi sekali. Pemeriksaan kedua atas token yang sama gagal dengan timeout-or-duplicate.

Tambahkan widget

Buat widget di dashboard Cloudflare di bagian Turnstile, daftarkan hostname tempat formulir Anda berjalan, lalu salin site key-nya. Kemudian tambahkan skrip dan elemen cf-turnstile di dalam formulir. Elemen itu harus berada di dalam <form>, atau field token tersembunyi akan berada di luarnya dan tidak pernah ikut terkirim.

contact.html
<form action="https://example.com/api/contact" method="POST">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-action="contact"></div>
  <button>Send message</button>
</form>

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
  • data-action="contact" memberi label pada widget. Siteverify mengembalikannya, sehingga server Anda bisa menolak token yang dibuat untuk formulir lain.
  • Skrip harus dimuat dari URL milik Cloudflare sendiri. Menurut Cloudflare, salinan yang di-proxy atau di-host sendiri merusak widget.
  • Jika situs Anda mengirim Content Security Policy, izinkan https://challenges.cloudflare.com di script-src dan frame-src.
  • Tanpa biaya, Cloudflare mengizinkan hingga 20 widget, masing-masing di hingga 10 hostname, jadi daftarkan juga host staging.

Verifikasi token di server Anda sendiri

Server mana pun bisa memverifikasi token: satu POST ke siteverify dengan secret dan token. Berikut contohnya sebagai Cloudflare Worker yang menjadi tujuan kiriman formulir statis. Worker memeriksa token, hostname, dan action, membuang token, lalu meneruskan field ke Ikiform dengan secret key. Ganti bagian penerusan dengan apa pun yang menyimpan atau mengirim email pesan itu.

src/index.ts
interface Env {
  TURNSTILE_SECRET_KEY: string;
  IKIFORM_SECRET_KEY: string;
}

interface Verdict {
  success: boolean;
  hostname?: string;
  action?: string;
  "error-codes"?: string[];
}

const SITE = "https://example.com";
const SITEVERIFY = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
    const form = await request.formData();

    const check = await fetch(SITEVERIFY, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        secret: env.TURNSTILE_SECRET_KEY,
        response: String(form.get("cf-turnstile-response") ?? ""),
        remoteip: request.headers.get("CF-Connecting-IP"),
        idempotency_key: crypto.randomUUID(),
      }),
    });
    const verdict = (await check.json()) as Verdict;
    const expected = verdict.hostname === new URL(SITE).hostname && verdict.action === "contact";
    if (!verdict.success || !expected) {
      console.warn("turnstile", verdict["error-codes"] ?? [], verdict.hostname, verdict.action);
      return Response.redirect(`${SITE}/contact?error=check`, 303);
    }

    form.delete("cf-turnstile-response");
    const sent = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        Accept: "application/json",
        Authorization: `Bearer ${env.IKIFORM_SECRET_KEY}`,
        Origin: request.headers.get("Origin") ?? SITE,
      },
      body: form,
    });
    return Response.redirect(sent.ok ? `${SITE}/thanks` : `${SITE}/contact?error=send`, 303);
  },
};
  • Siteverify menerima body JSON atau form-encoded dan selalu menjawab dengan JSON. remoteip opsional; idempotency_key memungkinkan Anda mengulang pemeriksaan dengan aman setelah error jaringan.
  • Periksa hostname dan action, bukan hanya success. Token yang dibuat di halaman orang lain dengan site key Anda juga menyatakan success.
  • Simpan kedua secret dengan wrangler secret put, jangan pernah di wrangler.toml.
  • Worker menjawab dengan redirect 303, jadi formulir biasa tidak butuh JavaScript. Arahkan action formulir ke route Worker.
  • Worker meneruskan header Origin milik halaman, karena fetch dari sisi server tidak mengirimnya dan Ikiform membacanya untuk mengenali uji gratis dari localhost.

Untuk menampilkan error di halaman formulir setelah pemeriksaan gagal, baca query string yang diatur Worker:

contact.html
<p id="form-error" role="alert" hidden></p>
<script>
  const reason = new URLSearchParams(location.search).get("error");
  const banner = document.getElementById("form-error");
  if (reason && banner) {
    banner.textContent =
      reason === "check" ? "The human check didn't pass. Try again." : "Your message didn't send. Try again.";
    banner.hidden = false;
  }
</script>
Versi sisi server mengirim dengan secret key, sehingga Ikiform melewati Allowed domains dan pemeriksaan manusianya. Semua kiriman lalu datang dari IP server Anda, dan batas Standard 5 per menit per pengunjung menghitung semua pengunjung Anda sebagai satu. Pilih rate limit Relaxed atau Custom untuk formulir yang Anda kirim lewat server, dan simpan IKIFORM_SECRET_KEY di environment variable khusus server.

Biarkan Ikiform yang memverifikasi

Jika Anda sama sekali tidak ingin menjalankan server, formulir bisa mengirim langsung ke Ikiform dan Ikiform yang melakukan panggilan siteverify. Pengaturan Spam check di setiap formulir menentukan caranya.

RuteKey yang Anda kelolaWidget di halaman AndaServer sendiri
Server atau Worker Anda sendiri, di atasSite key dan secret keyYa, milik AndaYa
Ikiform dengan Your own Turnstile keysSite key dan secret key, ditempel ke IkiformYa, milik AndaTidak
Ikiform dengan IkiformTidak adaTidak. Halaman pemeriksaan di ikiform.com, atau frame kecilTidak

Dengan Your own Turnstile keys, tempel site key dan secret key ke pengaturan formulir. Secret-nya dienkripsi saat disimpan. Pertahankan markup widget yang sama, tetapi arahkan formulir ke endpoint Ikiform Anda. Ikiform memverifikasi cf-turnstile-response dan mencocokkan hostname token dengan Allowed domains formulir. Kiriman tanpa token yang valid mendapat 403 captcha_failed, bahkan dengan secret API key.

contact.html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-action="contact"></div>
  <button>Send message</button>
</form>

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

Dengan Ikiform, Anda tidak menambahkan apa pun ke halaman. Kiriman HTML biasa tanpa token mendapat halaman singkat Checking you're human di ikiform.com yang menjalankan Turnstile lalu meneruskan field-nya. Kiriman fetch mendapat 403 challenge_required; panggil challenge() dari @ikiform/sdk untuk mendapatkan token terlebih dulu. Tidak ada akun Cloudflare yang terlibat. Dokumentasi spam menunjukkan kedua alur.

Jika Cloudflare tidak bisa dihubungi, Ikiform tidak membuang kiriman: kiriman disimpan sebagai spam dengan alasan captcha_unavailable, sehingga Anda bisa memulihkannya dari dashboard. Server Anda sendiri sebaiknya mengambil keputusan yang sama secara sadar, entah gagal secara tertutup seperti Worker di atas atau menyimpan kiriman untuk ditinjau.

Key uji

Cloudflare menerbitkan key dummy yang berfungsi di hostname apa pun, termasuk localhost. Pakai selama membangun, lalu ganti dengan key asli Anda.

KeyNilaiPerilaku
Site key1x00000000000000000000AASelalu lolos, widget terlihat
Secret key1x0000000000000000000000000000000AASiteverify selalu menjawab success
Secret key2x0000000000000000000000000000000AASiteverify selalu menjawab gagal

Token uji tidak membawa hostname asli Anda, jadi pemeriksaan hostname di Worker menolaknya. Longgarkan pemeriksaan itu hanya di environment pengembangan, jangan pernah di production.

Pastikan berfungsi

  1. Kirim formulir seperti biasa. Kiriman diterima dan widget tidak menampilkan apa-apa, atau hanya satu checkbox.
  2. Hapus elemen cf-turnstile lalu kirim. Kiriman ditolak: Worker me-redirect dengan ?error=check, atau Ikiform menjawab 403 captcha_failed.
  3. Kirim, tekan Back, lalu kirim halaman yang sama lagi. Token yang dipakai ulang gagal, yang menunjukkan server benar-benar memverifikasinya.
  4. Buka Submissions di dashboard. Kiriman baru ada di sana dengan setiap field yang Anda kirim.
  5. Tidak ada yang masuk? Ubah filter status di dashboard ke Spam. Spam mendapat jawaban sukses yang sama dengan kiriman asli.

Hal yang sering menjebak

  • Satu token, satu percobaan. Setelah pengiriman gagal yang ditangani dengan fetch, panggil turnstile.reset() untuk mendapatkan token baru. Memuat ulang halaman penuh otomatis mendapat token baru.
  • Pengetik lambat. Token kedaluwarsa setelah 300 detik. Pengaturan data-refresh-expired pada widget menentukan apakah widget mengambil token baru sendiri atau menunggu klik.
  • www dan domain apex. www.example.com dan example.com adalah hostname berbeda bagi siteverify. Bandingkan dengan yang benar-benar dipakai formulir Anda, atau izinkan keduanya.
  • Navigasi single-page. Skrip implisit merender widget sekali, saat dimuat. Saat route berubah di sisi client, render secara eksplisit dengan ?render=explicit dan turnstile.render().
  • Kiriman server dengan key Ikiform sendiri. Jika server Anda meneruskan ke Ikiform dengan secret key, Ikiform melewati pemeriksaan terkelolanya. Verifikasi token sendiri terlebih dulu, seperti yang dilakukan Worker.

Pertanyaan

Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.