Arrêter le spam des formulaires de contact7 min de lecture
Comment ajouter Cloudflare Turnstile à un formulaire HTML
Ajoutez Cloudflare Turnstile à un formulaire HTML simple : le widget, siteverify côté serveur, les clés de test, et une façon de l'utiliser sans clés ni serveur à vous.
Ajoutez le script de Turnstile et un élément cf-turnstile dans votre formulaire ; le widget place un token dans un champ masqué cf-turnstile-response. Envoyez ensuite ce token et votre clé secrète à l'endpoint siteverify de Cloudflare depuis un serveur, et n'acceptez l'envoi que s'il répond success. Sans serveur, laissez votre backend de formulaires le vérifier, ou utilisez la vérification gérée d'Ikiform, qui ne demande aucune clé.
Ce qui change avec Turnstile
Turnstile a deux moitiés, et un formulaire HTML simple n'a de place que pour une. Le widget tourne dans le navigateur avec votre site key publique et produit un token. La vérification tourne sur un serveur avec votre secret key, qui ne doit jamais atteindre la page. Un formulaire qui affiche le widget sans jamais vérifier le token n'est que décoration : un bot envoie directement vers votre endpoint et ignore complètement le widget.
La vraie question pour un formulaire HTML est donc de savoir où vit le secret. Cette page couvre trois réponses : votre propre petit serveur, un backend de formulaires qui détient vos clés, et une vérification gérée où personne d'autre que le backend ne détient de clés.
- Le widget Turnstile tourne dans le navigateur avec votre site key publique. La plupart des visiteurs ne le voient jamais rien faire.
- Il émet un token et le place dans un input masqué cf-turnstile-response à l'intérieur du formulaire.
- Le formulaire est envoyé, et le token voyage avec les autres champs.
- Votre serveur envoie la clé secrète, le token et l'IP du visiteur à l'endpoint siteverify de Cloudflare.
- Il vérifie que la réponse indique success et que le hostname et l'action sont ceux attendus.
- Si tout correspond, l'envoi est accepté. Si quelque chose cloche, il est refusé.
Ajouter le widget
Créez un widget dans le tableau de bord Cloudflare sous Turnstile, listez les hostnames où tourne votre formulaire, et copiez la site key. Ajoutez ensuite le script et un élément cf-turnstile dans le formulaire. L'élément doit être à l'intérieur du <form>, sinon le champ masqué du token se retrouve en dehors et n'est jamais envoyé.
<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"étiquette le widget. Siteverify le renvoie : votre serveur peut donc refuser un token émis pour un autre formulaire.- Le script doit être chargé depuis l'URL de Cloudflare. D'après Cloudflare, une copie passée par un proxy ou auto-hébergée casse le widget.
- Si votre site envoie une Content Security Policy, autorisez
https://challenges.cloudflare.comdansscript-srcet dansframe-src. - Sans frais, Cloudflare autorise jusqu'à 20 widgets, chacun sur jusqu'à 10 hostnames : listez donc aussi les hôtes de préproduction.
Vérifier le token sur votre propre serveur
N'importe quel serveur peut vérifier un token : un POST vers siteverify avec le secret et le token. Le voici sous forme de Cloudflare Worker vers lequel un formulaire statique envoie. Il vérifie le token, le hostname et l'action, retire le token, et transmet les champs à Ikiform avec une clé secrète. Remplacez cette transmission par tout autre moyen de stocker ou d'envoyer le message par e-mail.
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 accepte des corps JSON ou form-encoded et répond toujours en JSON.
remoteipest facultatif ;idempotency_keypermet de relancer une vérification sans risque après une erreur réseau. - Vérifiez
hostnameetaction, pas seulementsuccess. Un token émis sur la page de quelqu'un d'autre avec votre site key indique aussisuccess. - Stockez les deux secrets avec
wrangler secret put, jamais danswrangler.toml. - Le Worker répond par une redirection
303: le formulaire simple n'a donc besoin d'aucun JavaScript. Faites pointer l'actiondu formulaire vers la route du Worker. - Il transmet l'en-tête
Originde la page, car un fetch côté serveur n'en envoie aucun et Ikiform le lit pour reconnaître les tests gratuits depuislocalhost.
Pour afficher l'erreur sur la page du formulaire après un échec de vérification, lisez la query string que définit le 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 dans des variables d'environnement réservées au serveur.Laisser Ikiform le vérifier
Si vous ne voulez faire tourner aucun serveur, le formulaire peut envoyer directement à Ikiform, qui fait l'appel à siteverify. Le réglage Spam check de chaque formulaire décide comment.
| Chemin | Clés que vous gérez | Widget sur votre page | Serveur à vous |
|---|---|---|---|
| Votre propre serveur ou Worker, ci-dessus | Site key et clé secrète | Oui, le vôtre | Oui |
| Ikiform avec Your own Turnstile keys | Site key et clé secrète, collées dans Ikiform | Oui, le vôtre | Non |
| Ikiform avec Ikiform | Aucune | Non. Une page de vérification sur ikiform.com, ou un petit cadre | Non |
Avec Your own Turnstile keys, collez la site key et la clé secrète dans les réglages du formulaire. Le secret est chiffré au repos. Gardez le même balisage du widget, mais faites pointer le formulaire vers votre endpoint Ikiform. Ikiform vérifie cf-turnstile-response et compare le hostname du token aux Allowed domains du formulaire. Un envoi sans token valide reçoit 403 captcha_failed, même avec une clé API secrète.
<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>Avec Ikiform, vous n'ajoutez rien à la page. Un envoi HTML simple sans token reçoit une courte page Checking you're human sur ikiform.com qui exécute Turnstile et transmet les champs. Un envoi fetch reçoit plutôt 403 challenge_required ; appelez d'abord challenge() de @ikiform/sdk pour obtenir un token. Aucun compte Cloudflare n'entre en jeu. La documentation sur le spam montre les deux parcours.
Si Cloudflare est injoignable, Ikiform ne jette pas l'envoi : il le stocke comme spam avec la raison captcha_unavailable, pour que vous puissiez le restaurer depuis le tableau de bord. Votre propre serveur doit faire le même choix délibérément, soit en échouant de manière fermée comme le Worker ci-dessus, soit en gardant l'envoi pour relecture.
Clés de test
Cloudflare publie des clés factices qui fonctionnent sur n'importe quel hostname, y compris localhost. Utilisez-les pendant le développement, puis remplacez-les par vos vraies clés.
| Clé | Valeur | Comportement |
|---|---|---|
| Site key | 1x00000000000000000000AA | Passe toujours, widget visible |
| Clé secrète | 1x0000000000000000000000000000000AA | Siteverify répond toujours success |
| Clé secrète | 2x0000000000000000000000000000000AA | Siteverify répond toujours un échec |
Les tokens de test ne portent pas votre vrai hostname : la vérification du hostname du Worker les refuse donc. N'assouplissez cette vérification que dans votre environnement de développement, jamais en production.
Vérifiez que tout fonctionne
- Envoyez le formulaire normalement. L'envoi est accepté et le widget n'a rien affiché, ou une simple case à cocher.
- Retirez l'élément
cf-turnstileet envoyez. L'envoi est refusé : le Worker redirige avec?error=check, ou Ikiform répond403 captcha_failed. - Envoyez, revenez en arrière et renvoyez la même page. Le token réutilisé échoue, ce qui montre que le serveur le vérifie vraiment.
- Ouvrez Submissions dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.
- Rien n'est arrivé ? Passez le filtre de statut du tableau de bord sur Spam. Le spam reçoit la même réponse de succès qu'une vraie soumission.
Pièges
- Un token, un essai. Après un envoi échoué géré avec
fetch, appelezturnstile.reset()pour obtenir un nouveau token. Un rechargement complet de la page en obtient un automatiquement. - Les personnes qui tapent lentement. Les tokens expirent après 300 secondes. Le réglage
data-refresh-expireddu widget décide s'il en récupère un nouveau tout seul ou s'il attend un clic. - www et le domaine nu.
www.example.cometexample.comsont deux hostnames différents pour siteverify. Comparez avec celui où tourne réellement votre formulaire, ou autorisez les deux. - Navigation monopage. Le script implicite rend les widgets une seule fois, à son chargement. Lors d'un changement de route côté client, faites un rendu explicite avec
?render=explicitetturnstile.render(). - Envois serveur avec les clés d'Ikiform. Si votre serveur transmet à Ikiform avec une clé secrète, Ikiform saute sa vérification gérée. Vérifiez d'abord le token vous-même, comme le fait le Worker.
Questions
Le widget tourne n'importe où, mais le token doit être vérifié avec votre clé secrète sur un serveur. Sans serveur, envoyez le formulaire à un backend de formulaires qui le vérifie pour vous, ou utilisez la vérification gérée d'Ikiform, qui ne demande aucune clé.
Cloudflare le propose sans frais pour jusqu'à 20 widgets avec jusqu'à 10 hostnames chacun, et sa page des offres annonce des défis illimités. Consultez la page actuelle des offres de Cloudflare avant de vous y fier.
Le token a plus de 300 secondes ou a déjà été vérifié une fois. Affichez un nouveau widget, ou réinitialisez-le, après chaque tentative d'envoi.
Non. Réglez le Spam check du formulaire sur Ikiform et Ikiform exécute Turnstile sur ikiform.com. Il ne vous faut un compte que pour utiliser vos propres clés et votre propre widget.
Une étape vous bloque, ou vous voulez un guide pour votre cas ? Écrivez à hello@ikiform.com.