Créer un CAPTCHA mathématique en PHP : Coté client
La sécurité d’un formulaire en ligne repose sur plusieurs éléments, dans un premier temps, vous pouvez vous rapprocher de notre article Protection de base pour un formulaire. Par ailleurs, l’un des moyens les plus simples d’empêcher les robots d’envoyer des formulaires automatiquement est d’utiliser un CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart). Plutôt que d’intégrer une solution externe comme Google reCAPTCHA, Friendly Captcha, Cloudflare Turnstile ou tout autres alternatives, nous allons voir comment créer un CAPTCHA personnalisé en PHP, basé pour l’exemple sur une opération mathématique simple.
Comprendre le projet avant de se lancer
L’objectif est simple : afficher une image contenant une opération mathématique (par exemple, « 5 + 3 = ? »), demander à l’utilisateur d’entrer la réponse, et vérifier que la réponse est correcte avant de valider le formulaire. Toutefois, pour empêcher un robot de simplement extraire le texte de l’image, celle-ci sera brouillée avec divers éléments graphiques rendant la lecture plus difficile. Ainsi, l’algorithme de reconnaissance d’un robot ne pourra pas exploiter directement une donnée textuelle pour trouver la réponse.
Notre CAPTCHA devra donc avoir plusieurs caractéristiques :
- Une image générée dynamiquement en PHP
- Des dimensions adaptatives en fonction des besoins de l’affichage
- Du « bruit » graphique pour compliquer la lecture par des robots
- Un système sécurisé de stockage des réponses pour vérifier la saisie de l’utilisateur
Nous allons découper l’implémentation en plusieurs parties pour mieux comprendre chaque étape du processus, et éventuellement pouvoir les adapter à des besoins particuliers.
Dans cet article, nous allons nous concentrer sur la mise en place du CAPTCHA, en abordant la compréhension du projet ainsi que le développement de la partie client, incluant l’affichage dynamique et la gestion des interactions via JavaScript. La gestion globale côté serveur, incluant la génération de l’image CAPTCHA et la vérification des résultats, sera détaillée dans un prochain article afin d’explorer en profondeur ces aspects techniques.
Comprendre le fonctionnement global du CAPTCHA
Avant d’entrer dans les détails techniques, il est donc important de comprendre comment fonctionne notre CAPTCHA et les éléments qui le composent. L’un d’eux, le token, joue un rôle clé dans la validation du processus et nous allons voir pourquoi il est nécessaire.
Le principe de notre étude repose sur une image contenant une opération mathématique simple, que l’utilisateur doit résoudre pour prouver qu’il n’est pas un robot. Toutefois, pour assurer la sécurité du processus et empêcher toute tentative de soumission automatisée frauduleuse, nous devons associer chaque image générée à une réponse unique et temporaire. C’est ici qu’intervient le token.
Le rôle du token dans le système de CAPTCHA
Un token est un identifiant unique utilisé pour assurer une validation sécurisée dans divers contextes, notamment dans notre contexte il pourra associer une image CAPTCHA à une réponse correcte de manière temporaire et sécurisée.
Prenons un exemple de génération de token en PHP :
// Générer un token unique en hexadécimal
$token = bin2hex(random_bytes(16));
echo $token; // Exemple de sortie : "a3f9c7e2d6b45f89a12c3456e789abcd"
Lorsqu’un utilisateur sollicite et charge un CAPTCHA, un token est alors généré et stocké temporairement côté serveur associant l’image et la réponse correcte. Ce token est ensuite transmis au navigateur en même temps que l’image.
Lors de la soumission du formulaire, la réponse saisie par l’utilisateur est renvoyée avec ce token au serveur. Celui-ci vérifie alors si le token, agissant comme une clé, correspond bien à un CAPTCHA existant et si la réponse fournie est correcte. Une fois utilisé ou expiré, le token est supprimé pour éviter toute réutilisation frauduleuse.
Grâce à ce mécanisme, nous empêchons qu’un attaquant puisse répondre plusieurs fois au même CAPTCHA sans avoir à le résoudre.
Les fichiers nécessaires pour la mise en place du CAPTCHA
Maintenant que nous avons compris le rôle central du token, nous pouvons structurer notre projet en plusieurs fichiers distincts. Chacun d’eux jouant un rôle précis, garantissant ainsi une implémentation claire et modulaire :
form.html
: Contient le formulaire HTML où le CAPTCHA est affiché. Il comprend entre autres l’image du CAPTCHA, un champ pour la réponse de l’utilisateur, et un champ caché pour stocker le token.captcha.js
: Gère le chargement dynamique du CAPTCHA. Il envoie une requête àcaptcha_math.php
pour obtenir une nouvelle image et stocke le token correspondant dans le formulaire. Il permet aussi de recharger l’image si nécessaire.captcha_math.php
: Génère l’image CAPTCHA, crée un token unique, stocke la réponse correcte et renvoie le tout sous forme de JSON (image encodée en base64 + token).captcha_tokens.json
: Plusieurs utilisateurs pouvant interagir avec le formulaire simultanément, il est essentiel de stocker et gérer plusieurs tokens en parallèle. Ce fichier centralise donc temporairement les tokens actifs et leurs réponses associées, permettant une vérification efficace lors de la soumission des formulaires tout en assurant une gestion fluide des requêtes concurrentes.validate_captcha.php
: Vérifie la réponse de l’utilisateur en comparant le token soumis avec la réponse correcte stockée côté serveur.
Cette séparation des fichiers assure une meilleure organisation et une gestion plus efficace du CAPTCHA, tout en rendant l’ensemble facilement modifiable et extensible.
Le fragment HTML
Avant d’intégrer le CAPTCHA et son token dans notre formulaire form.html
, nous devons comprendre le rôle des balises HTML que nous allons devoir utiliser.
<form action="validate_captcha.php" method="post">
<!-- Autres balises du formulaire -->
<!-- Balises nécessaires pour la mise en place du CAPTCHA -->
<img id="captcha_image" src="" alt="CAPTCHA">
<input type="hidden" name="captcha_token" id="captcha_token">
<input type="text" name="captcha_input" id="captcha_input" required placeholder="Entrez le résultat">
<button type="button" onclick="loadCaptcha()">Rafraîchir</button>
<!-- Bouton d'envoi du formulaire -->
<button type="submit">Valider</button>
</form>
Ce fragment représente uniquement la partie du formulaire dédiée au CAPTCHA, en omettant volontairement les autres champs, ou mise en structure, pour rester concentrés sur l’essentiel. Il comprend donc :
- Une balise
<img>
qui affichera dynamiquement l’image du CAPTCHA générée côté serveur. - Un champ caché
<input type="hidden">
pour stocker le token associé à cette image, permettant ainsi la vérification côté serveur. - Un champ
<input type="text">
dans lequel l’utilisateur saisira la réponse à l’opération mathématique affichée. - Un bouton
<button>
permettant de rafraîchir l’image du CAPTCHA si celle-ci est trop difficile à lire.
Dans un contexte réel, ce fragment s’intègre dans un formulaire plus complet, comprenant bien entendu d’autres champs, ou d’autres balises structurelles. Enfin, ce formulaire envoie la réponse de l’utilisateur à validate_captcha.php
, où elle sera vérifiée avant validation du formulaire.
Mise en place du fichier JavaScript pour charger le CAPTCHA
Afin d’afficher dynamiquement notre CAPTCHA dès le chargement de la page, nous allons implémenter un fichier JavaScript captcha.js
qui enverra une requête au serveur pour obtenir l’image du CAPTCHA ainsi que son token associé. Cette requête invoquera le script serveur captcha_math.php
, en lui transmettant les dimensions spécifiques de l’image du CAPTCHA à générer (largeur et hauteur) sous forme de paramètres d’URL.
function loadCaptcha() {
// Récupération des divers éléments HTML nécessaires
const captchaImage = document.getElementById('captcha_image');
const captchaInput = document.getElementById('captcha_input');
const captchaToken = document.getElementById('captcha_token');
const container = captchaImage.parentElement; // Le conteneur de l'image
// Récupérer les styles calculés du conteneur
// Largeur réellement disponible pour l'image
const width = // calcul de la largeur;
// Hauteur fixe pour l'image, basée sur la hauteur du champ de saisie
const height = // calcul de la haueur;
// Effectuer une requête pour obtenir l'image et le token
fetch(`https://votreserveur.com/captcha_math.php?width=${width}&height=${height}`)
// Traitement de la réponse
}
// Charger initialement le CAPTCHA avec les dimensions
document.addEventListener('DOMContentLoaded', loadCaptcha);
- Récupération des éléments HTML : On sélectionne l’image, le champ de saisie et le champ caché qui contiendra le token.
- Déduction du container : Le conteneur nous donnera des précisions sur la taille de l’image à définir.
- Calcul des dimensions : On ajustera donc la largeur et la hauteur de l’image pour qu’elle s’adapte au mieux à son conteneur.
- Requête au serveur :
fetch()
envoie une requête àcaptcha_math.php
en lui fournissant les dimensions désirées. - Traitement de la réponse : Nous explorerons le traitement de la réponse ultérieurement
Ce script assure que le CAPTCHA s’affiche correctement dès le chargement de la page. Nous allons d’abord le compléter en finalisant le calcul des dimensions de l’image et en améliorant la gestion de la réponse du serveur. Ensuite, nous l’enrichirons en ajoutant un rafraîchissement manuel du CAPTCHA ainsi qu’une adaptation automatique en cas de redimensionnement de la fenêtre du navigateur.
Adapter les dimensions du CAPTCHA à son conteneur
Pour assurer une intégration fluide du CAPTCHA dans notre formulaire, nous devons adapter ses dimensions en fonction de la mise en page. Dans notre article, nous avons placé l’image directement dans le formulaire pour simplifier l’explication. Cependant, en pratique, la mise en forme repose souvent sur un système de grille, comme celui proposé par Bootstrap, qui organise les éléments du formulaire en rangées et colonnes.
Prenons un exemple de structuration en Bootstrap :
<div class="row">
<!-- Image CAPTCHA sur 4 colonnes -->
<div class="col-md-4">
<img id="captcha_image" src="" alt="CAPTCHA">
<input type="hidden" name="captcha_token" id="captcha_token">
</div>
<!-- Bouton de rechargement sur 2 colonnes -->
<div class="col-md-2">
<button type="button" onclick="reloadCaptcha()">Rafraîchir</button>
</div>
<!-- Champ de saisie sur 6 colonnes -->
<div class="col-md-6">
<input type="text" name="captcha_input" id="captcha_input" required placeholder="Entrez le résultat">
</div>
</div>
Ici, l’image du CAPTCHA n’est pas simplement un élément du formulaire, mais elle est intégrée dans une colonne spécifique (col-md-4
), distincte du champ de saisie et du bouton de rechargement. Cela signifie que son conteneur n’est plus le formulaire global, mais une structure HTML plus détaillée.
Calcul des dimensions disponibles
Puisque notre image est contenue dans une colonne spécifique, il est essentiel d’en récupérer les dimensions réelles et nécessaires avant de générer l’image CAPTCHA. Nous devons tenir compte des espaces internes (padding) et des bordures du conteneur afin d’obtenir la largeur réellement disponible pour l’image.
Voici comment nous récupérons ces valeurs en JavaScript, il suffit d’ajouter ces quelques lignes à notre fonction loadCaptcha()
:
// Récupérer les styles calculés du conteneur
const styles = window.getComputedStyle(container);
const paddingLeft = parseFloat(styles.paddingLeft) || 0;
const paddingRight = parseFloat(styles.paddingRight) || 0;
const borderLeft = parseFloat(styles.borderLeftWidth) || 0;
const borderRight = parseFloat(styles.borderRightWidth) || 0;
Une fois ces valeurs récupérées, nous pouvons calculer la largeur exacte que doit avoir notre image, et adapter le script contenu dans captcha.js
:
// Largeur réellement disponible pour l'image
const width = container.offsetWidth - paddingLeft - paddingRight - borderLeft - borderRight;
// Hauteur fixe pour l'image, basée sur la hauteur du champ de saisie
const height = captchaInput.offsetHeight;
Pourquoi cette approche ?
- Éviter que l’image ne dépasse de son conteneur
En soustrayant les marges et bordures, nous nous assurons que l’image ne déborde pas et s’adapte parfaitement à la grille du formulaire. - Assurer une cohérence visuelle
La hauteur de l’image étant alignée sur celle du champ de saisie, cela garantit une harmonie dans l’affichage. - Rendre le formulaire plus réactif
Si le formulaire est redimensionné (notamment en mode responsive), l’image sera recalculée dynamiquement en fonction des nouvelles dimensions.
Cette approche garantit que le CAPTCHA s’intègre de manière fluide et esthétique, tout en restant fonctionnel quelle que soit la structure HTML utilisée. Nous pouvons maintenant adapter
Adapter le CAPTCHA au redimensionnement de la fenêtre
Dans un contexte responsive, l’affichage du CAPTCHA doit s’adapter aux dimensions disponibles. Comme le texte du CAPTCHA est intégré dans une image, il ne peut pas s’ajuster dynamiquement comme un texte HTML classique. Lorsqu’un utilisateur redimensionne la fenêtre de son navigateur, l’image risque de ne plus être parfaitement alignée avec l’interface du formulaire.
Une solution efficace consiste à régénérer une nouvelle image à chaque redimensionnement significatif. Cela garantit une meilleure lisibilité et maintient l’adaptation aux nouvelles dimensions du conteneur. De plus, comme une nouvelle image est générée, un nouveau token est attribué, garantissant l’unicité du CAPTCHA.
Mise en place du rechargement automatique
Pour éviter une surcharge inutile du serveur à chaque pixel redimensionné, nous utilisons un délai avant rechargement. Ce délai assure que le CAPTCHA ne se régénère qu’après un redimensionnement stabilisé.
Nous commençons par définir un pointeur externe qui servira à contrôler le délai d’attente avant d’exécuter la régénération :
let resizeTimeout;
Ensuite, nous ajoutons un écouteur d’événement sur la fenêtre, qui surveille les changements de taille et déclenche un rechargement du CAPTCHA après un court délai (300 ms dans cet exemple) :
// Recharge le CAPTCHA après redimensionnement (avec un délai)
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout); // Annule les exécutions précédentes
resizeTimeout = setTimeout(reloadCaptcha, 300); // Recharge après 300 ms
});
Pourquoi cette approche ?
- Éviter un rechargement excessif
Sans délai, l’événementresize
serait déclenché de nombreuses fois en quelques millisecondes, surchargant inutilement le serveur avec des requêtes de régénération du CAPTCHA. - Optimiser la lisibilité et l’adaptation
En créant une nouvelle image adaptée aux nouvelles dimensions de l’écran, nous garantissons une meilleure lisibilité du CAPTCHA sans déformation. - Assurer un fonctionnement sécurisé
Chaque CAPTCHA régénéré obtient un nouveau token, empêchant toute réutilisation d’un CAPTCHA ancien et renforçant ainsi la protection du formulaire.
Cette technique permet d’intégrer un CAPTCHA réactif, capable de s’adapter aux changements d’affichage tout en restant sécurisé et optimisé pour l’utilisateur.
Finalisation de l’invocation du script captcha_math.php
Nous avons maintenant tous les éléments pour compléter l’appel au script captcha_math.php
. Ce script serveur sera chargé de générer une image CAPTCHA, de créer un token unique, et de retourner ces données au format JSON.
Anticipation de la réponse du serveur
Le serveur nous renverra une réponse contenant deux éléments :
image
: Une image encodée en base64, directement exploitable comme source (src
) pour l’élément<img>
.token
: Une clé unique qui sera stockée et envoyée avec le formulaire pour la validation côté serveur.
Sachant cela, nous pouvons d’ores et déjà adapter la requête fetch()
et sa gestion des données :
// Effectuer une requête pour obtenir l'image et le token
fetch(`https://shared.ymct.eu/email/captcha_math.php?width=${width}&height=${height}`)
.then(response => response.json()) // Convertir la réponse en JSON
.then(data => {
captchaImage.src = data.image; // Mettre à jour l'image CAPTCHA
captchaToken.value = data.token; // Mettre à jour le token caché
})
.catch(error => console.error('Erreur lors du chargement du CAPTCHA:', error));
- Envoi de la requête
- L’URL inclut les paramètres
width
etheight
pour générer une image adaptée aux dimensions du conteneur.
- L’URL inclut les paramètres
- Traitement de la réponse JSON
response.json()
: Convertit la réponse en objet JavaScript.data.image
: Contient l’image du CAPTCHA encodée en base64 (ex. :data:image/png;base64,...
).data.token
: Stocke le token associé au CAPTCHA, qui sera récupéré pour validation côté serveur.
- Mise à jour dynamique de l’interface
- L’attribut
src
decaptchaImage
est mis à jour pour afficher la nouvelle image. captchaToken.value
est rempli avec le token unique, prêt à être envoyé avec le formulaire.
- L’attribut
- Gestion des erreurs
- En cas d’échec de la requête (
catch
), un message d’erreur est affiché dans la console, facilitant le débogage.
- En cas d’échec de la requête (
Conclusion
Avec cette implémentation, nous avons maintenant un système robuste qui charge et rafraîchit dynamiquement le CAPTCHA en fonction des dimensions disponibles. Ce dernier point finalise la mise en place du système, en assurant une interaction fluide et sécurisée entre la partie client et le serveur.
Dans le prochain article, Créer un CAPTCHA mathématique en PHP : Coté serveur, nous détaillerons la génération de l’image côté serveur et la vérification de la réponse utilisateur pour compléter le processus de validation du CAPTCHA.