Intégrez reCAPTCHA v2 et v3 dans vos projets HTML/JS et Angular : configuration, validation côté serveur, best practices et alternatives avec exemples de code complets.
Pourquoi protéger ses formulaires avec reCAPTCHA
Les formulaires web sont la porte d'entrée principale des bots malveillants : spams, soumissions automatiques, brute force sur les pages de connexion, création de faux comptes en masse. Sans protection, un formulaire de contact ouvert peut recevoir des milliers de soumissions non désirées par heure.
reCAPTCHA est le service de protection anti-bot développé par Google. Il analyse le comportement de l'utilisateur (mouvements de souris, temps passé sur la page, historique du navigateur) pour distinguer un humain d'un robot, sans friction inutile pour l'utilisateur légitime.
Il existe deux versions principales, chacune adaptée à un contexte différent :
- reCAPTCHA v2 : affiche un widget visible (checkbox "Je ne suis pas un robot" ou défi image)
- reCAPTCHA v3 : complètement invisible, attribue un score de risque de 0.0 à 1.0
Créer votre compte reCAPTCHA
Avant d'intégrer reCAPTCHA, vous devez créer une paire de clés (site key + secret key) via la console Google. Ces deux clés sont indissociables de votre domaine.
Étapes de création
- Rendez-vous sur google.com/recaptcha/admin/create (connectez-vous avec un compte Google)
- Saisissez un label descriptif (ex : "Mon site - Formulaire contact")
- Choisissez le type de reCAPTCHA : v2 ou v3
- Ajoutez vos domaines autorisés (ex :
monsite.fr,localhost) - Acceptez les CGU et cliquez sur "Soumettre"
Les deux clés obtenues
| Clé | Nom | Utilisation | Visibilité |
|---|---|---|---|
Site Key |
Clé publique | Intégrée dans le HTML/JS côté client | Visible par tous (normal) |
Secret Key |
Clé secrète | Vérification côté serveur uniquement | Ne jamais exposer côté client |
Clés de test Google (développement)
Pour tester en local sans créer de compte, Google fournit des clés de test officielles :
// Clés de test Google (toujours acceptées en dev)
// Site Key (v2 checkbox) :
6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
// Secret Key (v2) :
6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
// ATTENTION : ces clés ne fonctionnent qu'avec "localhost"
// ou "127.0.0.1" — jamais en production
reCAPTCHA v2 — Intégration HTML/JS
reCAPTCHA v2 est la version la plus connue : l'utilisateur coche une case "Je ne suis pas un robot". Si le comportement est suspect, Google affiche un défi image (sélectionner les feux de circulation, etc.). Ce mode convient aux formulaires critiques comme les inscriptions ou les paiements.
Intégration HTML de base
<!-- Étape 1 : charger l'API reCAPTCHA de manière asynchrone -->
<!-- async et defer évitent de bloquer le rendu de la page -->
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<form id="contact-form" novalidate>
<div class="mb-3">
<label for="user-email" class="form-label">Votre email</label>
<input type="email" id="user-email" name="email"
class="form-control" required aria-required="true">
</div>
<!-- Étape 2 : placer le widget reCAPTCHA v2 -->
<!-- data-sitekey : votre clé publique -->
<!-- data-callback : fonction appelée quand l'utilisateur valide -->
<!-- data-expired-callback : fonction appelée si le token expire -->
<div class="g-recaptcha"
data-sitekey="VOTRE_SITE_KEY"
data-callback="onCaptchaSuccess"
data-expired-callback="onCaptchaExpired">
</div>
<!-- Le bouton est désactivé jusqu'à validation du CAPTCHA -->
<button type="submit" id="submit-btn"
class="btn btn-primary mt-3" disabled>
Envoyer le message
</button>
</form>
Callbacks JavaScript v2
// Callback déclenché quand l'utilisateur valide le CAPTCHA avec succès
function onCaptchaSuccess(token) {
// Activer le bouton de soumission
document.getElementById('submit-btn').disabled = false;
// Le token expire après 2 minutes — ne pas le stocker côté client
console.log('Token reCAPTCHA reçu (début) :', token.substring(0, 20) + '...');
}
// Callback déclenché si le token expire (durée de vie : 2 minutes)
function onCaptchaExpired() {
// Désactiver le bouton pour forcer une nouvelle validation
document.getElementById('submit-btn').disabled = true;
// Réinitialiser le widget pour permettre une nouvelle tentative
grecaptcha.reset();
console.warn('Token reCAPTCHA expiré — nouvelle validation requise');
}
// Gestion de la soumission du formulaire
document.getElementById('contact-form').addEventListener('submit', function(e) {
e.preventDefault();
// Récupérer le token généré par le widget
const token = grecaptcha.getResponse();
// Vérification supplémentaire côté client (non suffisante seule)
if (!token) {
console.error('Veuillez valider le CAPTCHA avant de soumettre');
return;
}
// Envoyer les données + token au serveur pour validation
fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: document.getElementById('user-email').value,
recaptchaToken: token // Toujours inclure le token dans la requête
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
console.log('Message envoyé avec succès');
grecaptcha.reset(); // Réinitialiser après succès
}
})
.catch(err => console.error('Erreur envoi :', err));
});
Mode "Invisible" (v2 Invisible)
reCAPTCHA v2 propose également un mode "Invisible" : aucun widget visible, le défi n'apparaît que si Google détecte un comportement suspect. La configuration est légèrement différente :
<!-- reCAPTCHA v2 Invisible : data-size="invisible" -->
<!-- La clé doit être configurée pour le type "Invisible" dans la console -->
<button class="g-recaptcha btn btn-primary"
data-sitekey="VOTRE_SITE_KEY_INVISIBLE"
data-callback="onInvisibleCaptchaSuccess"
data-action="submit">
Envoyer
</button>
<script>
// Callback pour le mode invisible
function onInvisibleCaptchaSuccess(token) {
// Soumettre automatiquement le formulaire une fois le token obtenu
document.getElementById('contact-form').submit();
}
</script>
reCAPTCHA v3 — Intégration avancée
reCAPTCHA v3 ne demande aucune interaction à l'utilisateur. Google analyse le comportement de navigation et retourne un score de 0.0 à 1.0 : 1.0 = très probablement humain, 0.0 = très probablement un bot. C'est vous qui décidez du seuil d'acceptation côté serveur.
Chargement de l'API v3
<!-- Charger l'API v3 avec votre site key directement dans l'URL -->
<!-- render=VOTRE_SITE_KEY initialise automatiquement le widget invisible -->
<script src="https://www.google.com/recaptcha/api.js?render=VOTRE_SITE_KEY"></script>
Exécution du token v3 côté JavaScript
// Fonction asynchrone pour soumettre un formulaire protégé par reCAPTCHA v3
async function soumettreFormulaire(e) {
e.preventDefault();
try {
// Exécuter reCAPTCHA v3 et obtenir le token
// L'action identifie l'opération dans la console Google (analytics par action)
const token = await grecaptcha.execute('VOTRE_SITE_KEY', {
action: 'contact_form' // Nom descriptif de l'action (ex: login, checkout, comment)
});
// Récupérer les données du formulaire
const formData = {
nom: document.getElementById('nom').value,
email: document.getElementById('email').value,
message: document.getElementById('message').value,
// Toujours inclure le token dans la requête serveur
recaptchaToken: token,
recaptchaAction: 'contact_form' // Pour vérification côté serveur
};
// Envoyer la requête au serveur
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
afficherSucces('Message envoyé avec succès !');
} else {
// Le serveur a refusé (score trop bas ou erreur)
afficherErreur('Vérification échouée. Veuillez réessayer.');
}
} catch (error) {
console.error('Erreur reCAPTCHA v3 :', error);
afficherErreur('Une erreur est survenue. Rechargez la page.');
}
}
// Attacher la fonction à la soumission du formulaire
document.getElementById('contact-form').addEventListener('submit', soumettreFormulaire);
Comprendre le score reCAPTCHA v3
| Score | Interprétation | Action recommandée |
|---|---|---|
| 0.9 – 1.0 | Très probablement humain | Accepter sans friction |
| 0.5 – 0.8 | Probablement humain | Accepter (seuil recommandé : 0.5) |
| 0.3 – 0.4 | Incertain | Proposer un défi v2 comme fallback |
| 0.0 – 0.2 | Très probablement un bot | Rejeter la soumission |
Intégration Angular avec ng-recaptcha
La bibliothèque ng-recaptcha est la solution officielle et la plus utilisée pour intégrer reCAPTCHA dans une application Angular. Elle supporte v2, v2 invisible et v3, avec une intégration native dans les Reactive Forms.
Installation
# Installer la bibliothèque ng-recaptcha via npm
npm install ng-recaptcha
# Vérifier l'installation dans package.json
# "ng-recaptcha": "^13.x.x" (compatible Angular 15+)
Configuration de l'environnement
// src/environments/environment.ts (développement)
export const environment = {
production: false,
// La site key est publique — elle peut être exposée côté client
// JAMAIS la secret key ici !
recaptchaSiteKey: '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI', // Clé de test
};
// src/environments/environment.prod.ts (production)
export const environment = {
production: true,
recaptchaSiteKey: '6LcXXXXXXXXXXXXXXXXXXXXXXXXXXXX', // Votre clé de prod
};
Configuration du Module Angular (v2)
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import {
RecaptchaModule, // Widget reCAPTCHA v2
RecaptchaFormsModule, // Intégration avec les Reactive Forms Angular
RECAPTCHA_SETTINGS,
RecaptchaSettings
} from 'ng-recaptcha';
import { environment } from '../environments/environment';
@NgModule({
declarations: [AppComponent, ContactComponent],
imports: [
BrowserModule,
ReactiveFormsModule,
RecaptchaModule, // Fournit le composant <re-captcha>
RecaptchaFormsModule, // Lie reCAPTCHA aux FormControl Angular
],
providers: [
{
provide: RECAPTCHA_SETTINGS,
// Centraliser la site key pour éviter de la répéter dans chaque template
useValue: {
siteKey: environment.recaptchaSiteKey,
} as RecaptchaSettings,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
Template Angular (contact.component.html)
<!-- contact.component.html -->
<form [formGroup]="contactForm" (ngSubmit)="onSubmit()" novalidate>
<!-- Champ message avec validation Angular -->
<div class="mb-3">
<label for="message" class="form-label">Votre message</label>
<textarea id="message" formControlName="message"
class="form-control" rows="4"
aria-required="true"></textarea>
<!-- Affichage conditionnel de l'erreur de validation -->
<div *ngIf="contactForm.get('message')?.invalid && contactForm.get('message')?.touched"
class="text-danger small mt-1">
Le message doit contenir au moins 10 caractères.
</div>
</div>
<!-- Widget reCAPTCHA v2 lié au Reactive Form via formControlName -->
<!-- (resolved) : émis quand le token est généré -->
<!-- (errored) : émis en cas d'erreur réseau ou configuration -->
<re-captcha
formControlName="recaptcha"
(resolved)="onCaptchaResolved($event)"
(errored)="onCaptchaError($event)"
aria-label="Validation anti-robot reCAPTCHA">
</re-captcha>
<!-- Bouton désactivé si le formulaire est invalide (CAPTCHA non résolu inclus) -->
<button type="submit"
class="btn btn-primary mt-3"
[disabled]="contactForm.invalid || isSubmitting"
aria-busy="isSubmitting">
<span *ngIf="!isSubmitting">Envoyer le message</span>
<span *ngIf="isSubmitting">Envoi en cours...</span>
</button>
</form>
Component TypeScript (contact.component.ts)
// contact.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-contact',
templateUrl: './contact.component.html'
})
export class ContactComponent implements OnInit {
contactForm!: FormGroup;
// La site key n'est pas nécessaire ici car configurée globalement via RECAPTCHA_SETTINGS
siteKey = environment.recaptchaSiteKey;
isSubmitting = false;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
// Initialisation du formulaire réactif
this.contactForm = this.fb.group({
message: [
'',
[Validators.required, Validators.minLength(10)] // Validation du message
],
// Le FormControl recaptcha est requis — le widget met à jour sa valeur automatiquement
// null = pas encore résolu, string = token valide
recaptcha: [null, Validators.required]
});
}
// Appelé par (resolved) du composant re-captcha
onCaptchaResolved(token: string | null): void {
// Le token est automatiquement stocké dans le FormControl via formControlName
// Cette méthode peut servir à des logs ou actions supplémentaires
if (token) {
console.log('CAPTCHA résolu — longueur du token :', token.length);
}
}
// Appelé par (errored) du composant re-captcha
onCaptchaError(error: unknown): void {
console.error('Erreur reCAPTCHA :', error);
// Réinitialiser le FormControl pour bloquer la soumission
this.contactForm.get('recaptcha')?.reset();
}
async onSubmit(): Promise {
// Vérification défensive — ne devrait pas passer si le template est correct
if (this.contactForm.invalid) return;
this.isSubmitting = true;
try {
// Envoyer les données du formulaire incluant le token reCAPTCHA
const payload = {
message: this.contactForm.get('message')?.value,
recaptchaToken: this.contactForm.get('recaptcha')?.value
};
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.success) {
// Réinitialiser le formulaire après succès
this.contactForm.reset();
console.log('Message envoyé avec succès');
}
} catch (error) {
console.error('Erreur lors de l\'envoi :', error);
} finally {
// Toujours réactiver le bouton même en cas d'erreur
this.isSubmitting = false;
}
}
}
reCAPTCHA v3 avec Angular (ng-recaptcha)
// Pour reCAPTCHA v3, importez RecaptchaV3Module au lieu de RecaptchaModule
import {
RecaptchaV3Module,
RECAPTCHA_V3_SITE_KEY,
ReCaptchaV3Service
} from 'ng-recaptcha';
// Dans app.module.ts
@NgModule({
imports: [RecaptchaV3Module],
providers: [
{
provide: RECAPTCHA_V3_SITE_KEY,
useValue: environment.recaptchaSiteKey // Clé configurée pour v3
}
]
})
export class AppModule {}
// Dans un service Angular
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { ReCaptchaV3Service } from 'ng-recaptcha';
@Injectable({ providedIn: 'root' })
export class ContactService {
constructor(
private http: HttpClient,
private recaptchaV3: ReCaptchaV3Service
) {}
// Méthode combinant reCAPTCHA v3 + appel API en une seule chaîne Observable
envoyerAvecRecaptcha(data: { message: string }): Observable<any> {
// execute() génère un token v3 pour l'action spécifiée
return this.recaptchaV3.execute('contact_form').pipe(
// switchMap reçoit le token et effectue la requête HTTP
switchMap(token =>
this.http.post('/api/contact', {
...data,
recaptchaToken: token, // Token v3 à vérifier côté serveur
recaptchaAction: 'contact_form' // Action pour validation croisée
})
)
);
}
}
Validation côté serveur
La validation côté serveur est non négociable. Un attaquant peut facilement contourner la validation JavaScript côté client. Le token reCAPTCHA doit être envoyé à l'API Google depuis votre serveur, avec votre secret key, avant de traiter le formulaire.
Validation en Node.js / Express
// server.js — Endpoint Express avec vérification reCAPTCHA
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
// Fonction de vérification du token reCAPTCHA via l'API Google
async function verifierRecaptcha(token, ip = null) {
// Ne jamais exposer la secret key dans le code source versionné
// Utiliser des variables d'environnement
const secretKey = process.env.RECAPTCHA_SECRET_KEY;
if (!secretKey) {
throw new Error('RECAPTCHA_SECRET_KEY non configurée dans les variables d\'environnement');
}
// Appel POST à l'API de vérification Google
const response = await axios.post(
'https://www.google.com/recaptcha/api/siteverify',
null, // Corps vide — paramètres passés en query string
{
params: {
secret: secretKey,
response: token, // Token reçu depuis le client
remoteip: ip // Optionnel mais recommandé pour la précision du score
}
}
);
const {
success,
score, // Présent uniquement pour v3 (0.0 à 1.0)
action, // Action renvoyée par v3 (à vérifier pour la cohérence)
'error-codes': errors
} = response.data;
// Pour reCAPTCHA v3 : vérifier le score minimum recommandé
if (score !== undefined && score < 0.5) {
throw new Error(`Score reCAPTCHA trop bas : ${score} (minimum 0.5 requis)`);
}
// Pour reCAPTCHA v2 : vérifier uniquement le succès
if (!success) {
throw new Error(`Vérification reCAPTCHA échouée : ${JSON.stringify(errors)}`);
}
// Retourner les données pour logs ou traitement conditionnel
return { success, score, action };
}
// Route de contact protégée par reCAPTCHA
app.post('/api/contact', async (req, res) => {
const { message, recaptchaToken } = req.body;
// Vérifier que le token est bien fourni dans la requête
if (!recaptchaToken) {
return res.status(400).json({ error: 'Token reCAPTCHA manquant' });
}
try {
// Vérification du token avant tout traitement métier
const captchaResult = await verifierRecaptcha(recaptchaToken, req.ip);
console.log('reCAPTCHA OK — score :', captchaResult.score);
// Ici : traiter le formulaire (envoi email, enregistrement BDD, etc.)
// ...
res.json({ success: true, message: 'Message envoyé avec succès' });
} catch (error) {
console.error('Erreur reCAPTCHA :', error.message);
res.status(400).json({ error: 'Vérification CAPTCHA échouée' });
}
});
Validation en PHP
<?php
// Fichier : api/contact.php
// Vérification du token reCAPTCHA côté serveur en PHP
/**
* Vérifie un token reCAPTCHA auprès de l'API Google.
*
* @param string $token Le token reçu depuis le formulaire client
* @param float $minScore Score minimum pour v3 (défaut : 0.5)
* @return bool true si la vérification est réussie
*/
function verifierRecaptcha(string $token, float $minScore = 0.5): bool {
// Charger la secret key depuis les variables d'environnement
// Ne JAMAIS écrire la clé en dur dans le code source
$secretKey = $_ENV['RECAPTCHA_SECRET_KEY'] ?? '';
if (empty($secretKey)) {
error_log('RECAPTCHA_SECRET_KEY non définie dans l\'environnement');
return false;
}
// Construire les paramètres de la requête POST
$data = http_build_query([
'secret' => $secretKey,
'response' => $token, // Token du formulaire client
'remoteip' => $_SERVER['REMOTE_ADDR'], // IP de l'utilisateur (optionnel)
]);
// Configurer le contexte HTTP pour la requête cURL simplifiée
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => $data,
'timeout' => 5, // 5 secondes maximum pour éviter les blocages
],
];
$context = stream_context_create($options);
$result = @file_get_contents(
'https://www.google.com/recaptcha/api/siteverify',
false,
$context
);
// Vérifier que la requête a abouti
if ($result === false) {
error_log('Impossible de contacter l\'API reCAPTCHA Google');
return false;
}
$response = json_decode($result, true);
// Pour reCAPTCHA v3 : vérifier le score minimum
if (isset($response['score'])) {
return $response['success'] === true && $response['score'] >= $minScore;
}
// Pour reCAPTCHA v2 : vérifier uniquement le succès
return $response['success'] === true;
}
// --- Traitement du formulaire ---
header('Content-Type: application/json');
// Accepter uniquement les requêtes POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Méthode non autorisée']);
exit;
}
// Récupérer et valider le corps de la requête JSON
$body = file_get_contents('php://input');
$data = json_decode($body, true);
$token = $data['recaptchaToken'] ?? '';
if (empty($token)) {
http_response_code(400);
echo json_encode(['error' => 'Token reCAPTCHA manquant']);
exit;
}
// Vérifier le token avant tout traitement
if (!verifierRecaptcha($token)) {
http_response_code(400);
echo json_encode(['error' => 'Vérification CAPTCHA échouée']);
exit;
}
// Ici : traiter le formulaire (envoi email, BDD, etc.)
$message = htmlspecialchars($data['message'] ?? '');
// Réponse de succès
echo json_encode(['success' => true, 'message' => 'Message reçu']);
Best practices et sécurité
1. Protéger la secret key
La secret key ne doit jamais apparaître dans le code source versionné, les logs, ou les réponses API :
# Stocker les clés dans les variables d'environnement
# Fichier .env (jamais commité dans git)
RECAPTCHA_SECRET_KEY=6LcXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# Ajouter .env au .gitignore
echo ".env" >> .gitignore
# En Node.js : charger avec dotenv
require('dotenv').config();
const secret = process.env.RECAPTCHA_SECRET_KEY;
# En PHP : charger avec $_ENV ou une lib .env
# Ne jamais faire : $secret = "6LcXXX...";
2. Configurer le Content Security Policy (CSP)
reCAPTCHA charge des scripts depuis les domaines Google. Configurez votre CSP en conséquence :
# Configuration Nginx — En-têtes CSP pour reCAPTCHA
# À ajouter dans le bloc server {} de votre config Nginx
add_header Content-Security-Policy "
default-src 'self';
script-src 'self'
https://www.google.com/recaptcha/
https://www.gstatic.com/recaptcha/;
frame-src 'self'
https://www.google.com/recaptcha/
https://recaptcha.google.com/recaptcha/;
style-src 'self'
https://www.gstatic.com/recaptcha/;
img-src 'self' data:
https://www.gstatic.com/;
";
3. Choisir la bonne version
- Utilisez v3 pour une UX maximale (pages produit, blog, recherche)
- Utilisez v2 checkbox pour les formulaires critiques (inscription, paiement)
- Combinez les deux : v3 en premier, v2 comme fallback si score < 0.5
- En développement : toujours utiliser les clés de test Google
4. Gérer l'expiration des tokens
// Le token reCAPTCHA expire après 2 minutes
// Pour les formulaires longs, prévoir une régénération automatique
// Option 1 : Régénérer avant soumission (v3)
document.getElementById('submit-btn').addEventListener('click', async (e) => {
e.preventDefault();
// Régénérer un token frais juste avant la soumission
const freshToken = await grecaptcha.execute('VOTRE_SITE_KEY', {
action: 'submit'
});
// Utiliser freshToken dans la requête — garantit qu'il est valide
soumettreAvecToken(freshToken);
});
// Option 2 : Écouter l'expiration (v2)
// Déjà géré via data-expired-callback="onCaptchaExpired"
5. Rate limiting complémentaire
reCAPTCHA ne remplace pas le rate limiting côté serveur. Combinez les deux :
// Middleware Express — Limiter à 5 requêtes/IP par 15 minutes
const rateLimit = require('express-rate-limit');
const contactLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // Fenêtre de 15 minutes
max: 5, // Maximum 5 requêtes par IP
message: { error: 'Trop de tentatives, réessayez dans 15 minutes' },
standardHeaders: true,
legacyHeaders: false,
});
// Appliquer le limiter avant la vérification reCAPTCHA
app.post('/api/contact', contactLimiter, async (req, res) => {
// ... vérification reCAPTCHA + traitement
});
6. Erreurs fréquentes à éviter
response.action === 'contact_form'.
Alternatives à reCAPTCHA
reCAPTCHA de Google est la solution la plus connue mais pas la seule. En fonction de vos contraintes (RGPD, vie privée, UX), d'autres solutions méritent votre attention.
Présentation des alternatives principales
hCaptcha
hCaptcha est une alternative directe à reCAPTCHA v2, compatible avec la même API. Sa principale différence : les revenus des défis CAPTCHA sont partiellement reversés aux propriétaires de sites. L'API d'intégration est quasi-identique à reCAPTCHA v2, ce qui facilite la migration.
<!-- Intégration hCaptcha (identique à reCAPTCHA v2 dans la structure) -->
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<!-- Remplacer class="g-recaptcha" par class="h-captcha" -->
<div class="h-captcha" data-sitekey="VOTRE_SITE_KEY_HCAPTCHA"></div>
<!-- Vérification serveur : même principe, URL différente -->
<!-- https://hcaptcha.com/siteverify (au lieu de google.com/recaptcha/api/siteverify) -->
Cloudflare Turnstile
Cloudflare Turnstile est gratuit, sans friction utilisateur visible, et ne collecte pas de données personnelles pour alimenter des modèles publicitaires. Il est particulièrement performant pour les sites déjà derrière Cloudflare.
<!-- Intégration Cloudflare Turnstile -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<!-- Widget Turnstile — cf-turnstile est la classe spécifique -->
<div class="cf-turnstile"
data-sitekey="VOTRE_SITE_KEY_TURNSTILE"
data-callback="onTurnstileSuccess">
</div>
<script>
// Callback appelé quand Turnstile génère un token valide
function onTurnstileSuccess(token) {
// Activer le formulaire — même logique que reCAPTCHA
document.getElementById('submit-btn').disabled = false;
}
</script>
Friendly Captcha
Friendly Captcha est une solution européenne, 100% RGPD conforme, sans cookies, sans tracking utilisateur. Le défi est résolu par le navigateur (proof of work) en arrière-plan, sans aucune interaction utilisateur. Solution payante mais recommandée pour les sites à forte conformité RGPD.
Tableau comparatif
| Solution | Prix | UX | RGPD | Intégration Angular |
|---|---|---|---|---|
| reCAPTCHA v2 | Gratuit (<1M/mois) | Widget visible | Partiel | ng-recaptcha |
| reCAPTCHA v3 | Gratuit (<1M/mois) | Invisible | Partiel | ng-recaptcha v3 |
| hCaptcha | Gratuit (usage raisonnable) | Widget visible | Meilleur | Compatible reCAPTCHA |
| Cloudflare Turnstile | Gratuit | Invisible/Minimal | Bon | Manuel (composant) |
| Friendly Captcha | Payant | Automatique | Excellent | Package npm disponible |
Conclusion
reCAPTCHA Google reste la solution anti-bot la plus simple à intégrer dans vos projets HTML/JS et Angular, avec un excellent rapport facilité/efficacité. La combinaison reCAPTCHA v3 (invisible, score) + v2 en fallback offre la meilleure expérience utilisateur tout en maintenant une protection robuste contre les bots.
La règle fondamentale à retenir : la validation côté serveur est non négociable. Que vous utilisiez reCAPTCHA, hCaptcha ou Turnstile, le token doit toujours être vérifié sur votre serveur avec votre secret key. Complétez cette protection par du rate limiting et une CSP bien configurée pour une sécurité en profondeur.