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.
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.
- Widget Turnstile berjalan di browser dengan site key publik Anda. Kebanyakan pengunjung tidak pernah melihatnya melakukan apa pun.
- Widget menerbitkan token dan menaruhnya di input tersembunyi cf-turnstile-response di dalam formulir.
- Formulir dikirim, dan token ikut bersama field lainnya.
- Server Anda mengirim secret key, token, dan IP pengunjung ke endpoint siteverify Cloudflare.
- Server memeriksa bahwa jawabannya success dan hostname serta action sesuai yang Anda harapkan.
- Jika semuanya cocok, kiriman diterima. Jika ada yang janggal, kiriman ditolak.
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.
<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.comdiscript-srcdanframe-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.
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.
remoteipopsional;idempotency_keymemungkinkan Anda mengulang pemeriksaan dengan aman setelah error jaringan. - Periksa
hostnamedanaction, bukan hanyasuccess. Token yang dibuat di halaman orang lain dengan site key Anda juga menyatakansuccess. - Simpan kedua secret dengan
wrangler secret put, jangan pernah diwrangler.toml. - Worker menjawab dengan redirect
303, jadi formulir biasa tidak butuh JavaScript. Arahkanactionformulir ke route Worker. - Worker meneruskan header
Originmilik halaman, karena fetch dari sisi server tidak mengirimnya dan Ikiform membacanya untuk mengenali uji gratis darilocalhost.
Untuk menampilkan error di halaman formulir setelah pemeriksaan gagal, baca query string yang diatur Worker:
<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>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.
| Rute | Key yang Anda kelola | Widget di halaman Anda | Server sendiri |
|---|---|---|---|
| Server atau Worker Anda sendiri, di atas | Site key dan secret key | Ya, milik Anda | Ya |
| Ikiform dengan Your own Turnstile keys | Site key dan secret key, ditempel ke Ikiform | Ya, milik Anda | Tidak |
| Ikiform dengan Ikiform | Tidak ada | Tidak. Halaman pemeriksaan di ikiform.com, atau frame kecil | Tidak |
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.
<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.
| Key | Nilai | Perilaku |
|---|---|---|
| Site key | 1x00000000000000000000AA | Selalu lolos, widget terlihat |
| Secret key | 1x0000000000000000000000000000000AA | Siteverify selalu menjawab success |
| Secret key | 2x0000000000000000000000000000000AA | Siteverify 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
- Kirim formulir seperti biasa. Kiriman diterima dan widget tidak menampilkan apa-apa, atau hanya satu checkbox.
- Hapus elemen
cf-turnstilelalu kirim. Kiriman ditolak: Worker me-redirect dengan?error=check, atau Ikiform menjawab403 captcha_failed. - Kirim, tekan Back, lalu kirim halaman yang sama lagi. Token yang dipakai ulang gagal, yang menunjukkan server benar-benar memverifikasinya.
- Buka Submissions di dashboard. Kiriman baru ada di sana dengan setiap field yang Anda kirim.
- 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, panggilturnstile.reset()untuk mendapatkan token baru. Memuat ulang halaman penuh otomatis mendapat token baru. - Pengetik lambat. Token kedaluwarsa setelah 300 detik. Pengaturan
data-refresh-expiredpada widget menentukan apakah widget mengambil token baru sendiri atau menunggu klik. - www dan domain apex.
www.example.comdanexample.comadalah 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=explicitdanturnstile.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
Widget bisa berjalan di mana saja, tetapi token harus diverifikasi dengan secret key Anda di server. Tanpa server, kirim formulir ke form backend yang memverifikasinya untuk Anda, atau pakai pemeriksaan terkelola Ikiform, yang tidak butuh key.
Cloudflare menyediakannya tanpa biaya untuk hingga 20 widget dengan masing-masing hingga 10 hostname, dan halaman paketnya mencantumkan tantangan tanpa batas. Cek halaman paket Cloudflare yang berlaku sebelum mengandalkannya.
Token berumur lebih dari 300 detik atau sudah pernah diverifikasi sekali. Render widget baru, atau reset, setelah setiap percobaan pengiriman.
Tidak. Atur Spam check formulir ke Ikiform dan Ikiform menjalankan Turnstile di ikiform.com. Anda hanya butuh akun untuk memakai key dan widget Anda sendiri.
Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.