Composant de Captcha pour la Connexion Frontend

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

  1. 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

  1. Installer l'extension VS Code "Live Server"
  2. 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');
})();

  1. 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>

  1. 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>

  1. 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
  1. 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%);

Étiquettes: JavaScript Vue3 CAPTCHA frontend Canvas

Publié le 27 juin à 00h07