Spam im Kontaktformular stoppen7 Min. Lesezeit
So fügst du Cloudflare Turnstile zu einem HTML-Formular hinzu
Füge Cloudflare Turnstile zu einem einfachen HTML-Formular hinzu: das Widget, serverseitiges siteverify, Testschlüssel und ein Weg ganz ohne eigene Schlüssel oder Server.
Füge das Turnstile-Skript und ein cf-turnstile-Element in dein Formular ein; das Widget legt ein Token in einem versteckten Feld cf-turnstile-response ab. Sende dann dieses Token und deinen Secret Key von einem Server an den siteverify-Endpunkt von Cloudflare und nimm den Post nur an, wenn er mit success antwortet. Ohne Server lass dein Formular-Backend es verifizieren oder nutze die verwaltete Prüfung von Ikiform, die gar keine Schlüssel braucht.
Was bei Turnstile anders ist
Turnstile hat zwei Hälften, und ein einfaches HTML-Formular hat nur für eine Platz. Das Widget läuft im Browser mit deinem öffentlichen site key und erzeugt ein Token. Die Prüfung läuft auf einem Server mit deinem secret key, der nie auf die Seite gelangen darf. Ein Formular, das das Widget zeigt, aber das Token nie verifiziert, ist Dekoration: Ein Bot sendet direkt an deinen Endpunkt und überspringt das Widget komplett.
Die eigentliche Frage für ein HTML-Formular ist also, wo das Secret liegt. Diese Seite behandelt drei Antworten: deinen eigenen kleinen Server, ein Formular-Backend, das deine Schlüssel verwahrt, und eine verwaltete Prüfung, bei der niemand außer dem Backend Schlüssel hat.
- Das Turnstile-Widget läuft im Browser mit deinem öffentlichen Site Key. Die meisten Besucher merken nichts davon.
- Es stellt ein Token aus und legt es in ein verstecktes Eingabefeld cf-turnstile-response im Formular.
- Das Formular wird gesendet, und das Token reist mit den anderen Feldern.
- Dein Server sendet den Secret Key, das Token und die IP des Besuchers an den siteverify-Endpunkt von Cloudflare.
- Er prüft, ob die Antwort success lautet und Hostname und Action die erwarteten sind.
- Passt alles, wird der Post angenommen. Stimmt irgendetwas nicht, wird er abgelehnt.
Das Widget hinzufügen
Leg im Cloudflare-Dashboard unter Turnstile ein Widget an, trag die Hostnamen ein, auf denen dein Formular läuft, und kopiere den Site Key. Füge dann das Skript und ein cf-turnstile-Element ins Formular ein. Das Element muss im <form> stehen, sonst landet das versteckte Token-Feld außerhalb und wird nie mitgesendet.
<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"beschriftet das Widget. Siteverify gibt es zurück, sodass dein Server ein Token ablehnen kann, das für ein anderes Formular erzeugt wurde.- Das Skript muss von Cloudflares eigener URL laden. Laut Cloudflare funktioniert das Widget mit einer über einen Proxy geleiteten oder selbst gehosteten Kopie nicht.
- Wenn deine Site eine Content Security Policy sendet, erlaube
https://challenges.cloudflare.comsowohl inscript-srcals auch inframe-src. - Kostenlos erlaubt Cloudflare bis zu 20 Widgets mit je bis zu 10 Hostnamen, also trag auch Staging-Hosts ein.
Das Token auf deinem eigenen Server verifizieren
Jeder Server kann ein Token verifizieren: ein POST an siteverify mit Secret und Token. Hier als Cloudflare Worker, an den ein statisches Formular sendet. Er prüft Token, Hostname und Action, entfernt das Token und leitet die Felder mit einem Secret Key an Ikiform weiter. Ersetze die Weiterleitung durch irgendetwas anderes, das die Nachricht speichert oder mailt.
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 akzeptiert JSON- oder form-encoded Bodys und antwortet immer mit JSON.
remoteipist optional; mitidempotency_keykannst du eine Prüfung nach einem Netzwerkfehler sicher wiederholen. - Prüfe
hostnameundaction, nicht nursuccess. Ein Token, das mit deinem Site Key auf der Seite eines anderen erzeugt wurde, sagt ebenfallssuccess. - Speichere beide Secrets mit
wrangler secret put, nie inwrangler.toml. - Der Worker antwortet mit einer
303-Weiterleitung, das einfache Formular braucht also kein JavaScript. Richte dieactiondes Formulars auf die Route des Workers aus. - Er reicht den
Origin-Header der Seite weiter, weil ein serverseitiges fetch keinen sendet und Ikiform ihn liest, um kostenloselocalhost-Tests zu erkennen.
Um nach einer fehlgeschlagenen Prüfung den Fehler auf der Formularseite anzuzeigen, lies den Query-String, den der Worker setzt:
<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 in reinen Server-Umgebungsvariablen.Ikiform verifizieren lassen
Wenn du gar keinen Server betreiben willst, kann das Formular direkt an Ikiform senden, und Ikiform übernimmt den siteverify-Aufruf. Die Einstellung Spam check jedes Formulars entscheidet, wie.
| Weg | Schlüssel, die du verwaltest | Widget auf deiner Seite | Eigener Server |
|---|---|---|---|
| Dein eigener Server oder Worker, siehe oben | Site Key und Secret Key | Ja, deiner | Ja |
| Ikiform mit Your own Turnstile keys | Site Key und Secret Key, in Ikiform eingefügt | Ja, deiner | Nein |
| Ikiform mit Ikiform | Keine | Nein. Eine Prüfseite auf ikiform.com oder ein kleiner Frame | Nein |
Bei Your own Turnstile keys fügst du Site Key und Secret Key in die Einstellungen des Formulars ein. Das Secret wird verschlüsselt gespeichert. Behalte dasselbe Widget-Markup, aber richte das Formular auf deinen Ikiform-Endpunkt aus. Ikiform verifiziert cf-turnstile-response und prüft den Hostnamen des Tokens gegen Allowed domains des Formulars. Ein Post ohne gültiges Token bekommt 403 captcha_failed, selbst mit einem geheimen API-Schlüssel.
<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>Bei Ikiform fügst du der Seite nichts hinzu. Ein einfacher HTML-Post ohne Token bekommt eine kurze Seite Checking you're human auf ikiform.com, die Turnstile ausführt und die Felder weiterschickt. Ein fetch-Post bekommt stattdessen 403 challenge_required; ruf zuerst challenge() aus @ikiform/sdk auf, um ein Token zu bekommen. Kein Cloudflare-Konto ist beteiligt. Die Spam-Dokumentation zeigt beide Abläufe.
Ist Cloudflare nicht erreichbar, verwirft Ikiform den Post nicht: Es speichert ihn mit dem Grund captcha_unavailable als Spam, damit du ihn im Dashboard wiederherstellen kannst. Dein eigener Server sollte dieselbe Entscheidung bewusst treffen: entweder sicher fehlschlagen wie der Worker oben oder den Post zur Durchsicht behalten.
Testschlüssel
Cloudflare veröffentlicht Dummy-Schlüssel, die auf jedem Hostnamen funktionieren, auch auf localhost. Nutze sie beim Bauen und tausche sie dann gegen deine echten Schlüssel aus.
| Schlüssel | Wert | Verhalten |
|---|---|---|
| Site Key | 1x00000000000000000000AA | Besteht immer, sichtbares Widget |
| Secret Key | 1x0000000000000000000000000000000AA | Siteverify antwortet immer mit Erfolg |
| Secret Key | 2x0000000000000000000000000000000AA | Siteverify antwortet immer mit Fehlschlag |
Test-Tokens tragen nicht deinen echten Hostnamen, daher lehnt die Hostnamen-Prüfung des Workers sie ab. Lockere diese Prüfung nur in deiner Entwicklungsumgebung, nie in der Produktion.
Prüfen, ob es funktioniert
- Sende das Formular normal ab. Der Post wird angenommen, und das Widget hat nichts gezeigt oder eine einzelne Checkbox.
- Entferne das
cf-turnstile-Element und sende ab. Der Post wird abgelehnt: Der Worker leitet mit?error=checkweiter, oder Ikiform antwortet mit403 captcha_failed. - Sende ab, geh zurück und sende dieselbe Seite erneut ab. Das wiederverwendete Token scheitert, was zeigt, dass der Server es wirklich verifiziert.
- Öffne Submissions im Dashboard. Die neue Einsendung ist da, mit jedem Feld, das du gesendet hast.
- Nichts angekommen? Stell den Statusfilter im Dashboard auf Spam. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.
Stolperfallen
- Ein Token, ein Versuch. Ruf nach einem fehlgeschlagenen, mit
fetchverarbeiteten Absendenturnstile.reset()für ein frisches Token auf. Ein vollständiges Neuladen der Seite holt automatisch eins. - Langsame Tipper. Tokens laufen nach 300 Sekunden ab. Die Widget-Einstellung
data-refresh-expiredentscheidet, ob es selbst ein neues holt oder auf einen Klick wartet. - www und die Apex-Domain.
www.example.comundexample.comsind für siteverify verschiedene Hostnamen. Vergleiche mit dem, auf dem dein Formular wirklich läuft, oder erlaube beide. - Single-Page-Navigation. Das implizite Skript rendert Widgets einmal, wenn es lädt. Rendere bei einem clientseitigen Routenwechsel explizit mit
?render=explicitundturnstile.render(). - Server-Posts mit Ikiforms eigenen Schlüsseln. Wenn dein Server mit einem Secret Key an Ikiform weiterleitet, überspringt Ikiform seine verwaltete Prüfung. Verifiziere das Token vorher selbst, wie es der Worker tut.
Fragen
Das Widget läuft überall, aber das Token muss auf einem Server mit deinem Secret Key verifiziert werden. Ohne Server sende das Formular an ein Formular-Backend, das es für dich verifiziert, oder nutze die verwaltete Prüfung von Ikiform, die keine Schlüssel braucht.
Cloudflare bietet es kostenlos für bis zu 20 Widgets mit je bis zu 10 Hostnamen an, und die Tarifseite nennt unbegrenzte Challenges. Prüfe die aktuelle Tarifseite von Cloudflare, bevor du dich darauf verlässt.
Das Token ist älter als 300 Sekunden oder wurde bereits einmal verifiziert. Rendere nach jedem Absendeversuch ein frisches Widget oder setze es zurück.
Nein. Setze die Spamprüfung des Formulars auf Ikiform, dann führt Ikiform Turnstile auf ikiform.com aus. Ein Konto brauchst du nur, um eigene Schlüssel und ein eigenes Widget zu nutzen.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.