Une solution de captcha de connexion entièrement frontend, proposant deux implémentations en JavaScript natif et Vue 3, incluant trois types de captcha : arithmétique, caractères aléatoires et curseur coulissant.
Démonstration des Effets
Captcha Arithmétique
Captcha à Caractères Aléatoires
Captcha à Curseur Coulissant
Structure des Répertoires
├── index.html # Point d'entrée version native
├── css/
│ └── style.css # Styles version native
├── js/
│ ├── main.js # Logique principale version native
│ └── captcha/
│ ├── captchaMath.js # Captcha arithmétique
│ ├── captchaCaractere.js # Captcha à caractères aléatoires
│ └── captchaCurseur.js # Captcha à curseur coulissant
│
└── vue-captcha/ # Version Vue 3
├── src/
│ ├── components/
│ │ ├── CaptchaMath.vue
│ │ ├── CaptchaCaractere.vue
│ │ └── CaptchaCurseur.vue
│ ├── App.vue
│ ├── main.js
│ └── style.css
├── index.html
├── package.json
└── vite.config.js
- Version JavaScript Nativement
1.1 Conditions Requises
- Navigateurs modernes (Chrome, Firefox, Edge, Safari)
- Pas besoin de Node.js ou d'outils de build
1.2 Installation et Exécution
Méthode 1 : Ouverture directe
Double-cliquez sur le fichier index.html et ouvrez-le dans le navigateur.
Méthode 2 : Serveur local
# Démarrer un serveur statique avec npx
npx serve .
# Accéder à http://localhost:3000
Méthode 3 : VS Code Live Server
- Installer l'extension VS Code "Live Server"
- Clic droit sur
index.html→ "Open with Live Server"
1.3 Code Complet
index.html - Page Principale
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connexion avec Captcha</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="conteneur-connexion">
<h2>Connexion Utilisateur</h2>
<form id="formulaireConnexion">
<div class="groupe-formulaire">
<label for="nomUtilisateur">Nom d'utilisateur</label>
<input type="text" id="nomUtilisateur" placeholder="Entrez votre nom" required>
</div>
<div class="groupe-formulaire">
<label for="motDePasse">Mot de passe</label>
<input type="password" id="motDePasse" placeholder="Entrez votre mot de passe" required>
</div>
<div class="groupe-formulaire">
<label>Type de Captcha</label>
<select id="typeCaptcha">
<option value="arithmetique">Arithmétique</option>
<option value="caractere">Caractères aléatoires</option>
<option value="curseur">Curseur coulissant</option>
</select>
</div>
<div id="zoneCaptcha" class="zone-captcha"></div>
<button type="submit" class="bouton-connexion">Se connecter</button>
</form>
<div id="message" class="message"></div>
</div>
<script src="js/captcha/captchaMath.js"></script>
<script src="js/captcha/captchaCaractere.js"></script>
<script src="js/captcha/captchaCurseur.js"></script>
<script src="js/main.js"></script>
</body>
</html>
css/style.css - Fichier de Styles
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #3498db 0%, #2c3e50 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.conteneur-connexion {
background: #fff;
padding: 40px;
border-radius: 10px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
width: 400px;
}
.conteneur-connexion h2 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.groupe-formulaire {
margin-bottom: 20px;
}
.groupe-formulaire label {
display: block;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.groupe-formulaire input,
.groupe-formulaire select {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
.groupe-formulaire input:focus,
.groupe-formulaire select:focus {
outline: none;
border-color: #3498db;
}
.zone-captcha {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
min-height: 80px;
}
.bouton-connexion {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #3498db 0%, #2c3e50 100%);
color: #fff;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
}
.bouton-connexion:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
}
.message {
margin-top: 20px;
padding: 10px;
border-radius: 5px;
text-align: center;
display: none;
}
.message.succes {
display: block;
background: #d4edda;
color: #155724;
}
.message.erreur {
display: block;
background: #f8d7da;
color: #721c24;
}
.captcha-math {
display: flex;
align-items: center;
gap: 10px;
}
.captcha-math .operation {
font-size: 18px;
font-weight: bold;
color: #333;
background: #e9ecef;
padding: 8px 15px;
border-radius: 5px;
user-select: none;
}
.captcha-math input {
width: 80px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
text-align: center;
}
.captcha-math .bouton-rafraichir {
padding: 8px 12px;
background: #3498db;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
.captcha-caractere {
display: flex;
align-items: center;
gap: 10px;
}
.captcha-caractere canvas {
border-radius: 5px;
cursor: pointer;
}
.captcha-caractere input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
.captcha-caractere .bouton-rafraichir {
padding: 8px 12px;
background: #3498db;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
.captcha-curseur {
position: relative;
}
.captcha-curseur .rail-curseur {
width: 100%;
height: 40px;
background: #e9ecef;
border-radius: 20px;
position: relative;
overflow: hidden;
}
.captcha-curseur .progression-curseur {
height: 100%;
background: linear-gradient(135deg, #3498db 0%, #2c3e50 100%);
width: 0;
border-radius: 20px;
transition: width 0.1s;
}
.captcha-curseur .bouton-curseur {
position: absolute;
top: 0;
left: 0;
width: 50px;
height: 40px;
background: #fff;
border-radius: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
cursor: grab;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}
.captcha-curseur .bouton-curseur:active {
cursor: grabbing;
}
.captcha-curseur .bouton-curseur::before {
content: '→';
font-size: 18px;
color: #3498db;
}
.captcha-curseur .texte-curseur {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #999;
font-size: 14px;
pointer-events: none;
}
.captcha-curseur.succes .rail-curseur {
background: #d4edda;
}
.captcha-curseur.succes .bouton-curseur::before {
content: '✓';
color: #28a745;
}
js/captcha/captchaMath.js - Captcha Arithmétique
/**
* Composant de captcha arithmétique
*
* Génère des opérations aléatoires (addition, soustraction, multiplication, division)
* avec des contraintes pour assurer la divisibilité et des résultats positifs.
*/
class CaptchaMath {
constructor(zone) {
this.zone = zone;
this.resultat = 0;
this.champSaisie = null;
this.initialiser();
}
initialiser() {
this.afficher();
this.genererOperation();
}
afficher() {
this.zone.innerHTML = `
<div class="captcha-math">
<span class="operation" id="operationMath"></span>
<span>=</span>
<input type="number" id="reponseMath" placeholder="?" maxlength="4">
<button type="button" class="bouton-rafraichir" id="rafraichirMath">Rafraîchir</button>
</div>
`;
this.elementOperation = document.getElementById('operationMath');
this.champSaisie = document.getElementById('reponseMath');
document.getElementById('rafraichirMath').addEventListener('click', () => {
this.genererOperation();
});
}
genererOperation() {
const operateurs = ['+', '-', '×', '÷'];
const operateur = operateurs[Math.floor(Math.random() * operateurs.length)];
let operande1, operande2;
switch (operateur) {
case '+':
operande1 = Math.floor(Math.random() * 60) + 1;
operande2 = Math.floor(Math.random() * 60) + 1;
this.resultat = operande1 + operande2;
break;
case '-':
operande1 = Math.floor(Math.random() * 80) + 20;
operande2 = Math.floor(Math.random() * operande1);
this.resultat = operande1 - operande2;
break;
case '×':
operande1 = Math.floor(Math.random() * 12) + 1;
operande2 = Math.floor(Math.random() * 12) + 1;
this.resultat = operande1 * operande2;
break;
case '÷':
operande2 = Math.floor(Math.random() * 8) + 2;
this.resultat = Math.floor(Math.random() * 15) + 1;
operande1 = operande2 * this.resultat;
break;
}
this.elementOperation.textContent = `${operande1} ${operateur} ${operande2}`;
this.champSaisie.value = '';
}
/**
* Vérifie si la réponse de l'utilisateur est correcte.
*/
verifier() {
const saisie = parseInt(this.champSaisie.value, 10);
return saisie === this.resultat;
}
/**
* Réinitialise le captcha avec une nouvelle opération.
*/
reinitialiser() {
this.genererOperation();
}
}
js/captcha/captchaCaractere.js - Captcha à Caractères Aléatoires
/**
* Composant de captcha à caractères aléatoires
*
* Utilise Canvas pour dessiner une image avec des caractères,
* des lignes et des points de perturbation pour augmenter la difficulté.
* La vérification est insensible à la casse.
*/
class CaptchaCaractere {
constructor(zone) {
this.zone = zone;
this.sequence = '';
this.canvas = null;
this.contexte = null;
this.champSaisie = null;
this.initialiser();
}
initialiser() {
this.afficher();
this.genererSequence();
}
afficher() {
this.zone.innerHTML = `
<div class="captcha-caractere">
<canvas id="canevasCaptcha" width="120" height="40" title="Cliquer pour rafraîchir"></canvas>
<input type="text" id="reponseCaractere" placeholder="Entrez le code" maxlength="4">
<button type="button" class="bouton-rafraichir" id="rafraichirCaractere">Rafraîchir</button>
</div>
`;
this.canvas = document.getElementById('canevasCaptcha');
this.contexte = this.canvas.getContext('2d');
this.champSaisie = document.getElementById('reponseCaractere');
this.canvas.addEventListener('click', () => this.genererSequence());
document.getElementById('rafraichirCaractere').addEventListener('click', () => this.genererSequence());
}
genererSequence() {
const caracteres = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
this.sequence = '';
for (let i = 0; i < 5; i++) {
this.sequence += caracteres.charAt(Math.floor(Math.random() * caracteres.length));
}
this.dessiner();
this.champSaisie.value = '';
}
dessiner() {
const ctx = this.contexte;
const canevas = this.canvas;
// Fond
ctx.fillStyle = this.couleurAleatoire(200, 255);
ctx.fillRect(0, 0, canevas.width, canevas.height);
// Texte du captcha
for (let i = 0; i < this.sequence.length; i++) {
ctx.font = `${Math.floor(Math.random() * 6) + 18}px Arial`;
ctx.fillStyle = this.couleurAleatoire(50, 150);
ctx.textBaseline = 'middle';
const posX = 10 + i * 22;
const posY = canevas.height / 2 + Math.random() * 8 - 4;
const angle = (Math.random() - 0.5) * 0.4;
ctx.save();
ctx.translate(posX, posY);
ctx.rotate(angle);
ctx.fillText(this.sequence[i], 0, 0);
ctx.restore();
}
// Lignes de perturbation
for (let i = 0; i < 5; i++) {
ctx.strokeStyle = this.couleurAleatoire(100, 200);
ctx.beginPath();
ctx.moveTo(Math.random() * canevas.width, Math.random() * canevas.height);
ctx.lineTo(Math.random() * canevas.width, Math.random() * canevas.height);
ctx.stroke();
}
// Points de perturbation
for (let i = 0; i < 40; i++) {
ctx.fillStyle = this.couleurAleatoire(0, 255);
ctx.beginPath();
ctx.arc(Math.random() * canevas.width, Math.random() * canevas.height, 1, 0, 2 * Math.PI);
ctx.fill();
}
}
couleurAleatoire(min, max) {
const rouge = Math.floor(Math.random() * (max - min) + min);
const vert = Math.floor(Math.random() * (max - min) + min);
const bleu = Math.floor(Math.random() * (max - min) + min);
return `rgb(${rouge}, ${vert}, ${bleu})`;
}
verifier() {
return this.champSaisie.value.toLowerCase() === this.sequence.toLowerCase();
}
reinitialiser() {
this.genererSequence();
}
}
js/captcha/captchaCurseur.js - Captcha à Curseur Coulissant
/**
* Composant de captcha à curseur coulissant
*
* Supporte le glisser-déposer et le toucher pour les appareils mobiles.
* La vérification réussit lorsque le curseur atteint 90% de la course.
*/
class CaptchaCurseur {
constructor(zone) {
this.zone = zone;
this.estVerifie = false;
this.glissement = false;
this.departX = 0;
this.positionX = 0;
this.largeurPiste = 0;
this.largeurBouton = 50;
this.seuil = 0.9;
this.initialiser();
}
initialiser() {
this.afficher();
this.lierEvenements();
}
afficher() {
this.zone.innerHTML = `
<div class="captcha-curseur" id="captchaCurseur">
<div class="rail-curseur" id="railCurseur">
<div class="progression-curseur" id="progressionCurseur"></div>
<div class="bouton-curseur" id="boutonCurseur"></div>
<span class="texte-curseur" id="texteCurseur">Glisser vers la droite</span>
</div>
</div>
`;
this.elementCaptcha = document.getElementById('captchaCurseur');
this.elementRail = document.getElementById('railCurseur');
this.elementProgression = document.getElementById('progressionCurseur');
this.elementBouton = document.getElementById('boutonCurseur');
this.elementTexte = document.getElementById('texteCurseur');
this.largeurPiste = this.elementRail.offsetWidth - this.largeurBouton;
}
lierEvenements() {
this.elementBouton.addEventListener('mousedown', (e) => this.debutGlissement(e));
document.addEventListener('mousemove', (e) => this.mouvementGlissement(e));
document.addEventListener('mouseup', () => this.finGlissement());
this.elementBouton.addEventListener('touchstart', (e) => this.debutGlissement(e));
document.addEventListener('touchmove', (e) => this.mouvementGlissement(e));
document.addEventListener('touchend', () => this.finGlissement());
}
debutGlissement(e) {
if (this.estVerifie) return;
this.glissement = true;
this.departX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
this.elementBouton.style.transition = 'none';
this.elementProgression.style.transition = 'none';
}
mouvementGlissement(e) {
if (!this.glissement) return;
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
let deplacement = clientX - this.departX;
deplacement = Math.max(0, Math.min(deplacement, this.largeurPiste));
this.positionX = deplacement;
this.elementBouton.style.left = deplacement + 'px';
this.elementProgression.style.width = (deplacement + this.largeurBouton) + 'px';
if (deplacement > 10) {
this.elementTexte.style.opacity = '0';
}
}
finGlissement() {
if (!this.glissement) return;
this.glissement = false;
this.elementBouton.style.transition = 'left 0.3s';
this.elementProgression.style.transition = 'width 0.3s';
if (this.positionX >= this.largeurPiste * this.seuil) {
this.estVerifie = true;
this.elementBouton.style.left = this.largeurPiste + 'px';
this.elementProgression.style.width = '100%';
this.elementCaptcha.classList.add('succes');
this.elementTexte.textContent = 'Vérification réussie';
this.elementTexte.style.opacity = '1';
this.elementTexte.style.color = '#28a745';
} else {
this.positionX = 0;
this.elementBouton.style.left = '0';
this.elementProgression.style.width = '0';
this.elementTexte.style.opacity = '1';
}
}
verifier() {
return this.estVerifie;
}
reinitialiser() {
this.estVerifie = false;
this.positionX = 0;
this.elementCaptcha.classList.remove('succes');
this.elementBouton.style.left = '0';
this.elementProgression.style.width = '0';
this.elementTexte.textContent = 'Glisser vers la droite';
this.elementTexte.style.opacity = '1';
this.elementTexte.style.color = '#999';
}
}
js/main.js - Logique Principale
/**
* Logique principale pour la gestion du formulaire de connexion
* et la sélection des types de captcha.
*/
(function() {
const zoneCaptcha = document.getElementById('zoneCaptcha');
const selecteurType = document.getElementById('typeCaptcha');
const formulaire = document.getElementById('formulaireConnexion');
const elementMessage = document.getElementById('message');
let captchaActif = null;
function initialiserCaptcha(type) {
zoneCaptcha.innerHTML = '';
switch (type) {
case 'arithmetique':
captchaActif = new CaptchaMath(zoneCaptcha);
break;
case 'caractere':
captchaActif = new CaptchaCaractere(zoneCaptcha);
break;
case 'curseur':
captchaActif = new CaptchaCurseur(zoneCaptcha);
break;
}
}
function afficherMessage(texte, type) {
elementMessage.textContent = texte;
elementMessage.className = 'message ' + type;
setTimeout(() => {
elementMessage.className = 'message';
}, 3000);
}
selecteurType.addEventListener('change', function() {
initialiserCaptcha(this.value);
});
formulaire.addEventListener('submit', function(e) {
e.preventDefault();
const nomUtilisateur = document.getElementById('nomUtilisateur').value.trim();
const motDePasse = document.getElementById('motDePasse').value.trim();
if (!nomUtilisateur || !motDePasse) {
afficherMessage('Veuillez remplir tous les champs', 'erreur');
return;
}
if (!captchaActif.verifier()) {
afficherMessage('Captcha incorrect, veuillez réessayer', 'erreur');
captchaActif.reinitialiser();
return;
}
afficherMessage('Connexion réussie !', 'succes');
setTimeout(() => {
formulaire.reset();
captchaActif.reinitialiser();
}, 2000);
});
initialiserCaptcha('arithmetique');
})();
- Version Vue 3
2.1 Conditions Requises
- Node.js >= 18.0.0
- npm >= 9.0.0
2.2 Installation et Exécution
# Naviguer vers le répertoire du projet
cd vue-captcha
# Installer les dépendances
npm install
# Démarrer le serveur de développement
npm run dev
# Accéder à http://localhost:5173
2.3 Construction pour la Production
# Construire l'application
npm run build
# Prévisualiser le résultat
npm run preview
2.4 Code Complet
package.json - Configuration du Projet
{
"name": "vue-captcha",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.26"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.5"
}
}
vite.config.js - Configuration Vite
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
})
index.html - Fichier d'Entrée
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connexion Vue 3 avec Captcha</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
src/main.js - Point d'Entrée Vue
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
createApp(App).mount('#app')
src/style.css - Styles Globaux
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #3498db 0%, #2c3e50 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.conteneur-connexion {
background: #fff;
padding: 40px;
border-radius: 10px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
width: 400px;
}
.conteneur-connexion h2 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.groupe-formulaire {
margin-bottom: 20px;
}
.groupe-formulaire label {
display: block;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.groupe-formulaire input,
.groupe-formulaire select {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
.groupe-formulaire input:focus,
.groupe-formulaire select:focus {
outline: none;
border-color: #3498db;
}
.zone-captcha {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
min-height: 80px;
}
.bouton-connexion {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #3498db 0%, #2c3e50 100%);
color: #fff;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
}
.bouton-connexion:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
}
.message {
margin-top: 20px;
padding: 10px;
border-radius: 5px;
text-align: center;
}
.message.succes {
background: #d4edda;
color: #155724;
}
.message.erreur {
background: #f8d7da;
color: #721c24;
}
.captcha-math {
display: flex;
align-items: center;
gap: 10px;
}
.captcha-math .operation {
font-size: 18px;
font-weight: bold;
color: #333;
background: #e9ecef;
padding: 8px 15px;
border-radius: 5px;
user-select: none;
}
.captcha-math input {
width: 80px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
text-align: center;
}
.bouton-rafraichir {
padding: 8px 12px;
background: #3498db;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
.captcha-caractere {
display: flex;
align-items: center;
gap: 10px;
}
.captcha-caractere canvas {
border-radius: 5px;
cursor: pointer;
}
.captcha-caractere input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
.captcha-curseur {
position: relative;
}
.rail-curseur {
width: 100%;
height: 40px;
background: #e9ecef;
border-radius: 20px;
position: relative;
overflow: hidden;
}
.progression-curseur {
height: 100%;
background: linear-gradient(135deg, #3498db 0%, #2c3e50 100%);
border-radius: 20px;
transition: width 0.1s;
}
.bouton-curseur {
position: absolute;
top: 0;
width: 50px;
height: 40px;
background: #fff;
border-radius: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
cursor: grab;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
transition: left 0.3s;
}
.bouton-curseur:active {
cursor: grabbing;
}
.bouton-curseur::before {
content: '→';
font-size: 18px;
color: #3498db;
}
.texte-curseur {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #999;
font-size: 14px;
pointer-events: none;
}
.captcha-curseur.succes .rail-curseur {
background: #d4edda;
}
.captcha-curseur.succes .bouton-curseur::before {
content: '✓';
color: #28a745;
}
.captcha-curseur.succes .texte-curseur {
color: #28a745;
}
src/App.vue - Composant Principal
<template>
<div class="conteneur-connexion">
<h2>Connexion Utilisateur</h2>
<form @submit.prevent="gererSoumission">
<div class="groupe-formulaire">
<label>Nom d'utilisateur</label>
<input v-model="formulaire.nomUtilisateur" type="text" placeholder="Entrez votre nom" required>
</div>
<div class="groupe-formulaire">
<label>Mot de passe</label>
<input v-model="formulaire.motDePasse" type="password" placeholder="Entrez votre mot de passe" required>
</div>
<div class="groupe-formulaire">
<label>Type de Captcha</label>
<select v-model="typeCaptcha">
<option value="arithmetique">Arithmétique</option>
<option value="caractere">Caractères aléatoires</option>
<option value="curseur">Curseur coulissant</option>
</select>
</div>
<div class="zone-captcha">
<CaptchaMath v-if="typeCaptcha === 'arithmetique'" ref="refCaptcha" />
<CaptchaCaractere v-else-if="typeCaptcha === 'caractere'" ref="refCaptcha" />
<CaptchaCurseur v-else ref="refCaptcha" />
</div>
<button type="submit" class="bouton-connexion">Se connecter</button>
</form>
<div v-if="notification.texte" :class="['message', notification.type]">
{{ notification.texte }}
</div>
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import CaptchaMath from './components/CaptchaMath.vue'
import CaptchaCaractere from './components/CaptchaCaractere.vue'
import CaptchaCurseur from './components/CaptchaCurseur.vue'
const formulaire = reactive({
nomUtilisateur: '',
motDePasse: ''
})
const typeCaptcha = ref('arithmetique')
const refCaptcha = ref(null)
const notification = reactive({ texte: '', type: '' })
const afficherNotification = (texte, type) => {
notification.texte = texte
notification.type = type
setTimeout(() => {
notification.texte = ''
}, 3000)
}
const gererSoumission = () => {
if (!formulaire.nomUtilisateur || !formulaire.motDePasse) {
afficherNotification('Veuillez remplir tous les champs', 'erreur')
return
}
if (!refCaptcha.value?.verifier()) {
afficherNotification('Captcha incorrect, veuillez réessayer', 'erreur')
refCaptcha.value?.reinitialiser()
return
}
afficherNotification('Connexion réussie !', 'succes')
setTimeout(() => {
formulaire.nomUtilisateur = ''
formulaire.motDePasse = ''
refCaptcha.value?.reinitialiser()
}, 2000)
}
watch(typeCaptcha, () => {
notification.texte = ''
})
</script>
src/components/CaptchaMath.vue - Composant Captcha Arithmétique
<template>
<div class="captcha-math">
<span class="operation">{{ operation }}</span>
<span>=</span>
<input v-model="reponseUtilisateur" type="number" placeholder="?" maxlength="4">
<button type="button" class="bouton-rafraichir" @click="generer">Rafraîchir</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const operation = ref('')
const resultat = ref(0)
const reponseUtilisateur = ref('')
const operateurs = ['+', '-', '×', '÷']
const generer = () => {
const operateur = operateurs[Math.floor(Math.random() * operateurs.length)]
let operande1, operande2
switch (operateur) {
case '+':
operande1 = Math.floor(Math.random() * 60) + 1
operande2 = Math.floor(Math.random() * 60) + 1
resultat.value = operande1 + operande2
break
case '-':
operande1 = Math.floor(Math.random() * 80) + 20
operande2 = Math.floor(Math.random() * operande1)
resultat.value = operande1 - operande2
break
case '×':
operande1 = Math.floor(Math.random() * 12) + 1
operande2 = Math.floor(Math.random() * 12) + 1
resultat.value = operande1 * operande2
break
case '÷':
operande2 = Math.floor(Math.random() * 8) + 2
resultat.value = Math.floor(Math.random() * 15) + 1
operande1 = operande2 * resultat.value
break
}
operation.value = `${operande1} ${operateur} ${operande2}`
reponseUtilisateur.value = ''
}
const verifier = () => {
return parseInt(reponseUtilisateur.value, 10) === resultat.value
}
const reinitialiser = () => {
generer()
}
onMounted(() => {
generer()
})
defineExpose({ verifier, reinitialiser })
</script>
src/components/CaptchaCaractere.vue - Composant Captcha à Caractères
<template>
<div class="captcha-caractere">
<canvas ref="refCanevas" width="120" height="40" title="Cliquer pour rafraîchir" @click="generer"></canvas>
<input v-model="reponseUtilisateur" type="text" placeholder="Entrez le code" maxlength="5">
<button type="button" class="bouton-rafraichir" @click="generer">Rafraîchir</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const refCanevas = ref(null)
const sequence = ref('')
const reponseUtilisateur = ref('')
const caracteres = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'
const couleurAleatoire = (min, max) => {
const rouge = Math.floor(Math.random() * (max - min) + min)
const vert = Math.floor(Math.random() * (max - min) + min)
const bleu = Math.floor(Math.random() * (max - min) + min)
return `rgb(${rouge}, ${vert}, ${bleu})`
}
const generer = () => {
sequence.value = ''
for (let i = 0; i < 5; i++) {
sequence.value += caracteres.charAt(Math.floor(Math.random() * caracteres.length))
}
dessiner()
reponseUtilisateur.value = ''
}
const dessiner = () => {
const canevas = refCanevas.value
const ctx = canevas.getContext('2d')
ctx.fillStyle = couleurAleatoire(200, 255)
ctx.fillRect(0, 0, canevas.width, canevas.height)
for (let i = 0; i < sequence.value.length; i++) {
ctx.font = `${Math.floor(Math.random() * 6) + 18}px Arial`
ctx.fillStyle = couleurAleatoire(50, 150)
ctx.textBaseline = 'middle'
const posX = 10 + i * 22
const posY = canevas.height / 2 + Math.random() * 8 - 4
const angle = (Math.random() - 0.5) * 0.4
ctx.save()
ctx.translate(posX, posY)
ctx.rotate(angle)
ctx.fillText(sequence.value[i], 0, 0)
ctx.restore()
}
for (let i = 0; i < 5; i++) {
ctx.strokeStyle = couleurAleatoire(100, 200)
ctx.beginPath()
ctx.moveTo(Math.random() * canevas.width, Math.random() * canevas.height)
ctx.lineTo(Math.random() * canevas.width, Math.random() * canevas.height)
ctx.stroke()
}
for (let i = 0; i < 40; i++) {
ctx.fillStyle = couleurAleatoire(0, 255)
ctx.beginPath()
ctx.arc(Math.random() * canevas.width, Math.random() * canevas.height, 1, 0, 2 * Math.PI)
ctx.fill()
}
}
const verifier = () => {
return reponseUtilisateur.value.toLowerCase() === sequence.value.toLowerCase()
}
const reinitialiser = () => {
generer()
}
onMounted(() => {
generer()
})
defineExpose({ verifier, reinitialiser })
</script>
src/components/CaptchaCurseur.vue - Composant Captcha à Curseur
<template>
<div :class="['captcha-curseur', { succes: estVerifie }]">
<div class="rail-curseur" ref="refRail">
<div class="progression-curseur" :style="{ width: largeurProgression }"></div>
<div
class="bouton-curseur"
:style="{ left: positionBouton }"
@mousedown="debutGlissement"
@touchstart="debutGlissement"
></div>
<span class="texte-curseur">{{ estVerifie ? 'Vérification réussie' : 'Glisser vers la droite' }}</span>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const refRail = ref(null)
const estVerifie = ref(false)
const glissement = ref(false)
const departX = ref(0)
const positionX = ref(0)
const largeurPiste = ref(0)
const largeurBouton = 50
const seuil = 0.9
const positionBouton = ref('0px')
const largeurProgression = ref('0px')
const debutGlissement = (e) => {
if (estVerifie.value) return
glissement.value = true
departX.value = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX
}
const mouvementGlissement = (e) => {
if (!glissement.value) return
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX
let deplacement = clientX - departX.value
deplacement = Math.max(0, Math.min(deplacement, largeurPiste.value))
positionX.value = deplacement
positionBouton.value = deplacement + 'px'
largeurProgression.value = (deplacement + largeurBouton) + 'px'
}
const finGlissement = () => {
if (!glissement.value) return
glissement.value = false
if (positionX.value >= largeurPiste.value * seuil) {
estVerifie.value = true
positionBouton.value = largeurPiste.value + 'px'
largeurProgression.value = '100%'
} else {
positionX.value = 0
positionBouton.value = '0px'
largeurProgression.value = '0px'
}
}
const verifier = () => estVerifie.value
const reinitialiser = () => {
estVerifie.value = false
positionX.value = 0
positionBouton.value = '0px'
largeurProgression.value = '0px'
}
onMounted(() => {
largeurPiste.value = refRail.value.offsetWidth - largeurBouton
document.addEventListener('mousemove', mouvementGlissement)
document.addEventListener('mouseup', finGlissement)
document.addEventListener('touchmove', mouvementGlissement)
document.addEventListener('touchend', finGlissement)
})
onUnmounted(() => {
document.removeEventListener('mousemove', mouvementGlissement)
document.removeEventListener('mouseup', finGlissement)
document.removeEventListener('touchmove', mouvementGlissement)
document.removeEventListener('touchend', finGlissement)
})
defineExpose({ verifier, reinitialiser })
</script>
- Dcoumentation de l'API des Composants
3.1 Interface Commune
Tous les composants de captcha implémentent une interface unifiée :
| Méthode | Paramètres | Valeur de Retour | Description |
|---|---|---|---|
verifier() |
Aucun | boolean |
Vérifie si l'entrée de l'utilisateur est correcte |
reinitialiser() |
Aucun | void |
Réinitialise l'état du captcha et génère un nouveau défi |
3.2 Utilisation de la Version Nativement
// Créer une instance
const conteneur = document.getElementById('zoneCaptcha');
const captcha = new CaptchaMath(conteneur);
// ou new CaptchaCaractere(conteneur);
// ou new CaptchaCurseur(conteneur);
// Vérifier
if (captcha.verifier()) {
console.log('Vérification réussie');
} else {
console.log('Échec de la vérification');
captcha.reinitialiser();
}
3.3 Utilisation de la Version Vue 3
<template>
<CaptchaMath ref="refCaptcha" />
<button @click="verifierCaptcha">Vérifier</button>
</template>
<script setup>
import { ref } from 'vue'
import CaptchaMath from './components/CaptchaMath.vue'
const refCaptcha = ref(null)
const verifierCaptcha = () => {
if (refCaptcha.value.verifier()) {
console.log('Vérification réussie')
} else {
console.log('Échec de la vérification')
refCaptcha.value.reinitialiser()
}
}
</script>
- Comparaison des Types de Captcha
| Type | Sécurité | Expérience Utilisateur | Scénarios d'Utilisation | Caractéristiques |
|---|---|---|---|---|
| Arithmétique | ⭐⭐ | ⭐⭐⭐ | Protection simple | Convivial, nécessite un calcul mental |
| Caractères aléatoires | ⭐⭐⭐ | ⭐⭐ | Vérification traditionnelle | Utilisé couramment, éléments de perturbation |
| Curseur coulissant | ⭐⭐ | ⭐⭐⭐⭐ | Appareils mobiles | Bonne expérience, opération simple |
- Personnalisation
5.1 Modiifer la Difficulté Arithmétique
Dans CaptchaMath, ajustez les plages numériques :
// Changement des plages pour l'addition
operande1 = Math.floor(Math.random() * 100) + 1;
operande2 = Math.floor(Math.random() * 100) + 1;
// Changement des plages pour la multiplication
operande1 = Math.floor(Math.random() * 20) + 1;
operande2 = Math.floor(Math.random() * 20) + 1;
5.2 Modifier la Longueur des Caractères
Dans CaptchaCaractere, changez le nombre de caractères :
// Générer 6 caractères
for (let i = 0; i < 6; i++) {
sequence += caracteres.charAt(Math.floor(Math.random() * caracteres.length));
}
5.3 Modifier le Seuil du Curseur
Dans CaptchaCurseur, ajustez le seuil :
// Passer à 80% pour la validation
this.seuil = 0.8;
5.4 Modifier les Couleurs du Thème
Dans les fichiers CSS, modifiez le dégradé :
/* Couleur principale */
background: linear-gradient(135deg, #3498db 0%, #2c3e50 100%);
/* Thème vert */
background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
/* Thème orange */
background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%);