首頁 雲端運算與程式碼文章正文

如何使用HTML+JavaScript實現滑動驗證碼_前端滑動驗證碼

雲端運算與程式碼 2026年02月13日 13:42 97 品悟

本文摘要

此示例使用 HTML + Canvas 實現滑動拼圖驗證碼。Canvas繪制帶隨機缺口的背景,獨立滑塊按鈕監聽滑鼠/觸摸拖拽,實時將滑塊位置映射到Canvas並繪制移動拼塊。松開時計算滑塊與缺口水平距離,在容差內即驗證通過,失敗則回彈。代碼包含重置功能,可刷新缺口位置。註意:此為前端演示,無服務端校驗,真實應用需加密傳輸軌跡並二次驗證。

以下是使用 HTML + JavaScript(Canvas) 實現滑動拼圖驗證碼的完整示例。該示例模擬了主流滑動驗證碼的核心流程:隨機生成缺口、拖拽滑塊、驗證對齊。

 1. 原理說明

如何使用HTML+JavaScript實現滑動驗證碼_前端滑動驗證碼 第1张

- 背景底板:Canvas 繪制純色背景或圖片,並“摳出”一個缺口(矩形或異形)。  

- 滑塊:可拖動的拼圖塊,初始位於左側起始區域。  

- 交互:監聽滑鼠/觸摸事件,實時更新滑塊的水平位置。  

- 驗證:松開滑鼠時,計算滑塊當前位置與缺口位置的差值,若在容差範圍內則通過。

> 生產環境還需增加行為軌跡驗證、加密參數、服務端二次校驗等安全措施,本例僅演示前端交互邏輯。

 2. 完整示例代碼

將以下代碼保存為 `.html` 文件,用瀏覽器打開即可體驗。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>滑動拼圖驗證碼(Canvas實現)</title>
    <style>
        body {
            font-family: sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background: #f0f2f5;
            margin: 0;
        }
        .captcha-container {
            text-align: center;
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 5px 20px rgba(0,0,0,0.1);
        }
        canvas {
            display: block;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            margin-bottom: 15px;
            cursor: pointer;
        }
        .slider-track {
            position: relative;
            width: 300px;
            height: 40px;
            background: #e9ecef;
            border-radius: 20px;
            margin: 0 auto;
            user-select: none;
        }
        .slider-button {
            position: absolute;
            width: 40px;
            height: 40px;
            background: #4a90e2;
            border-radius: 50%;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            cursor: grab;
            transition: box-shadow 0.1s;
            left: 0;
            top: 0;
        }
        .slider-button:active {
            cursor: grabbing;
            box-shadow: 0 4px 8px rgba(0,0,0,0.3);
        }
        .result {
            margin-top: 20px;
            font-size: 16px;
            color: #333;
            min-height: 24px;
        }
        .btn-reset {
            margin-top: 15px;
            padding: 8px 20px;
            background: #6c757d;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
        }
        .btn-reset:hover {
            background: #5a6268;
        }
    </style>
</head>
<body>
<div>
    <h3>滑動拼圖驗證碼</h3>
    <!-- 拼圖底板 Canvas -->
    <canvas id="puzzleCanvas" width="300" height="150"></canvas>
    <!-- 滑動軌道與滑塊(獨立於 Canvas,便於拖拽) -->
    <div id="sliderTrack">
        <div id="sliderButton"></div>
    </div>
    <div id="resultMessage"></div>
    <button id="resetBtn">刷新驗證</button>
</div>
<script>
    (function() {
        // -- 配置參數 --
        const canvas = document.getElementById('puzzleCanvas');
        const ctx = canvas.getContext('2d');
        const track = document.getElementById('sliderTrack');
        const button = document.getElementById('sliderButton');
        const resultMsg = document.getElementById('resultMessage');
        // 缺口參數
        const GAP_WIDTH = 40;          // 缺口寬度
        const GAP_HEIGHT = 40;         // 缺口高度
        const GAP_Y = 55;             // 缺口垂直位置(固定,更真實可隨機)
        const TOLERANCE = 5;          // 允許的誤差像素
        // 滑塊參數(與缺口大小一致)
        const SLIDER_SIZE = 40;        // 滑塊寬高
        // 狀態變量
        let gapX = 0;                // 缺口左上角的X坐標(隨機生成)
        let isDragging = false;      // 是否正在拖拽
        let startMouseX = 0;         // 拖拽起始滑鼠X
        let startButtonLeft = 0;     // 拖拽起始按鈕left值
        let verified = false;       // 是否已驗證成功
        // 軌道相關尺寸
        const trackWidth = track.clientWidth;
        const buttonWidth = button.clientWidth;
        const maxLeft = trackWidth - buttonWidth; // 滑塊最大可移動距離
        // -- 初始化 / 重置 --
        function resetCaptcha() {
            // 隨機生成缺口X坐標(範圍:左邊距10px ~ 右邊距-缺口寬度-10px)
            const minGapX = 10;
            const maxGapX = canvas.width - GAP_WIDTH - 10;
            gapX = Math.floor(Math.random() * (maxGapX - minGapX + 1) + minGapX);
            // 重置滑塊位置
            button.style.left = '0px';
            // 重置驗證狀態
            verified = false;
            resultMsg.innerHTML = '';
            resultMsg.style.color = '#333';
            // 繪制底板
            drawPuzzle();
        }
        // -- 繪制拼圖底板(包含缺口)--
        function drawPuzzle() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            // 繪制背景(模擬圖片,此處用漸變色)
            const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
            gradient.addColorStop(0, '#9fa8da');
            gradient.addColorStop(0.5, '#80cbc4');
            gradient.addColorStop(1, '#ffcc80');
            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            // 增加一些噪點/紋理,更逼真
            ctx.fillStyle = 'rgba(255,255,255,0.3)';
            for (let i = 0; i < 50; i++) {
                ctx.beginPath();
                ctx.arc(Math.random() * canvas.width, Math.random() * canvas.height, 1, 0, 2 * Math.PI);
                ctx.fill();
            }
            // -- 繪制缺口(鏤空效果)--
            ctx.save();
            // 缺口區域用深色陰影表示凹陷
            ctx.shadowColor = 'rgba(0,0,0,0.5)';
            ctx.shadowBlur = 4;
            ctx.shadowOffsetX = 2;
            ctx.shadowOffsetY = 2;
            ctx.fillStyle = '#2c3e50';   // 深色填充缺口
            ctx.fillRect(gapX, GAP_Y, GAP_WIDTH, GAP_HEIGHT);
            // 邊緣高光,增加立體感
            ctx.shadowBlur = 0;
            ctx.shadowOffsetX = 0;
            ctx.shadowOffsetY = 0;
            ctx.strokeStyle = 'rgba(255,255,255,0.6)';
            ctx.lineWidth = 1;
            ctx.strokeRect(gapX + 0.5, GAP_Y + 0.5, GAP_WIDTH - 1, GAP_HEIGHT - 1);
            ctx.restore();
            // 在缺口位置上方寫個“缺口”提示(僅演示,實際不需要)
            ctx.font = '12px Arial';
            ctx.fillStyle = 'white';
            ctx.shadowColor = 'black';
            ctx.shadowBlur = 2;
            ctx.fillText('⬅ 拖滑塊對齊這裏', gapX - 70, GAP_Y - 8);
            ctx.shadowBlur = 0;
            // 繪制起始標記(滑塊初始位置)
            ctx.fillStyle = 'rgba(74,144,226,0.3)';
            ctx.fillRect(2, GAP_Y, SLIDER_SIZE, SLIDER_SIZE);
            ctx.strokeStyle = '#4a90e2';
            ctx.lineWidth = 1.5;
            ctx.strokeRect(2, GAP_Y, SLIDER_SIZE, SLIDER_SIZE);
        }
        // -- 拖拽滑塊邏輯 --
        function onDragStart(e) {
            if (verified) return; // 已驗證成功,不可再拖
            e.preventDefault();
            isDragging = true;
            // 記錄開始時的滑鼠X位置(兼容觸摸)
            const clientX = e.type.startsWith('touch') ? e.touches[0].clientX : e.clientX;
            startMouseX = clientX;
            // 獲取當前按鈕的 left 值(可能帶px)
            const leftStr = window.getComputedStyle(button).left;
            startButtonLeft = parseFloat(leftStr) || 0;
            // 綁定全局事件
            document.addEventListener('mousemove', onDragMove);
            document.addEventListener('mouseup', onDragEnd);
            document.addEventListener('touchmove', onDragMove, { passive: false });
            document.addEventListener('touchend', onDragEnd);
        }
        function onDragMove(e) {
            if (!isDragging) return;
            e.preventDefault();
            const clientX = e.type.startsWith('touch') ? e.touches[0].clientX : e.clientX;
            const deltaX = clientX - startMouseX; // 滑鼠移動差值
            let newLeft = startButtonLeft + deltaX;
            // 限制範圍 0 ~ maxLeft
            newLeft = Math.max(0, Math.min(newLeft, maxLeft));
            // 更新滑塊位置
            button.style.left = newLeft + 'px';
            // 實時在Canvas上繪制滑塊跟隨位置(增強反饋)
            drawSliderOnCanvas(newLeft);
        }
        // 在Canvas上繪制滑塊(拼圖塊)實時位置
        function drawSliderOnCanvas(buttonLeft) {
            // 重繪底板
            drawPuzzle();
            // 將按鈕的left值映射到Canvas上的X坐標
            // 滑塊在Canvas上的X範圍:左側起始2px,右側截止 canvas.width - SLIDER_SIZE - 2
            const minSliderX = 2;
            const maxSliderX = canvas.width - SLIDER_SIZE - 2;
            // 按鈕left: 0 ~ maxLeft,映射到 sliderX
            const sliderX = minSliderX + (buttonLeft / maxLeft) * (maxSliderX - minSliderX);
            // 繪制滑塊(帶陰影和邊框)
            ctx.save();
            ctx.shadowColor = 'rgba(0,0,0,0.3)';
            ctx.shadowBlur = 5;
            ctx.shadowOffsetX = 2;
            ctx.shadowOffsetY = 2;
            ctx.fillStyle = '#4a90e2';
            ctx.fillRect(sliderX, GAP_Y, SLIDER_SIZE, SLIDER_SIZE);
            // 增加內發光效果
            ctx.shadowBlur = 0;
            ctx.strokeStyle = '#ffffff';
            ctx.lineWidth = 2;
            ctx.strokeRect(sliderX + 1, GAP_Y + 1, SLIDER_SIZE - 2, SLIDER_SIZE - 2);
            // 在滑塊上繪制箭頭圖案(提示可拖動)
            ctx.font = '18px Arial';
            ctx.fillStyle = 'white';
            ctx.shadowColor = 'rgba(0,0,0,0.5)';
            ctx.shadowBlur = 3;
            ctx.fillText('⇨', sliderX + 10, GAP_Y + 28);
            ctx.restore();
        }
        function onDragEnd(e) {
            if (isDragging) {
                // 驗證對齊
                verifyMatch();
            }
            isDragging = false;
            // 移除全局事件
            document.removeEventListener('mousemove', onDragMove);
            document.removeEventListener('mouseup', onDragEnd);
            document.removeEventListener('touchmove', onDragMove);
            document.removeEventListener('touchend', onDragEnd);
        }
        // 驗證滑塊是否與缺口對齊
        function verifyMatch() {
            // 獲取當前按鈕left值
            const leftStr = window.getComputedStyle(button).left;
            const currentLeft = parseFloat(leftStr) || 0;
            // 將按鈕left映射到Canvas上的滑塊X坐標
            const minSliderX = 2;
            const maxSliderX = canvas.width - SLIDER_SIZE - 2;
            const sliderX = minSliderX + (currentLeft / maxLeft) * (maxSliderX - minSliderX);
            // 計算滑塊與缺口的水平距離
            const diff = Math.abs(sliderX - gapX);
            if (diff <= TOLERANCE) {
                verified = true;
                resultMsg.innerHTML = '✅ 驗證通過!';
                resultMsg.style.color = 'green';
                // 驗證成功後,將滑塊固定在缺口位置(視覺上)
                drawSuccessState();
            } else {
                resultMsg.innerHTML = `❌ 驗證失敗,請再試一次 (誤差${Math.round(diff)}px)`;
                resultMsg.style.color = 'red';
                // 失敗後將滑塊彈回原位
                button.style.left = '0px';
                drawPuzzle(); // 重繪底板,清除滑塊繪制
            }
        }
        // 驗證成功後的展示:滑塊嵌合到缺口
        function drawSuccessState() {
            drawPuzzle(); // 重繪底板
            ctx.save();
            // 在缺口位置繪制藍色滑塊,表示已對齊
            ctx.fillStyle = '#4a90e2';
            ctx.shadowColor = 'rgba(0,0,0,0.3)';
            ctx.shadowBlur = 5;
            ctx.fillRect(gapX, GAP_Y, SLIDER_SIZE, SLIDER_SIZE);
            ctx.shadowBlur = 0;
            ctx.strokeStyle = '#fff';
            ctx.lineWidth = 2;
            ctx.strokeRect(gapX + 1, GAP_Y + 1, SLIDER_SIZE - 2, SLIDER_SIZE - 2);
            ctx.restore();
            // 滑塊按鈕變綠色,並鎖定
            button.style.background = '#28a745';
            button.style.cursor = 'default';
        }
        // -- 重置 --
        function resetAll() {
            // 重置滑塊樣式
            button.style.left = '0px';
            button.style.background = '#4a90e2';
            button.style.cursor = 'grab';
            resetCaptcha();
        }
        // -- 事件綁定 --
        function bindEvents() {
            // 拖拽開始:滑鼠/觸摸
            button.addEventListener('mousedown', onDragStart);
            button.addEventListener('touchstart', onDragStart, { passive: false });
            // 防止拖拽時選中頁面其他文字
            button.addEventListener('dragstart', (e) => e.preventDefault());
            // 重置按鈕
            document.getElementById('resetBtn').addEventListener('click', resetAll);
        }
        // 初始化
        resetCaptcha();
        bindEvents();
        // 窗口大小變化時重新計算軌道寬度(但本例尺寸固定,可不處理)
        // 如果父容器寬度變化,需動態更新maxLeft,此處略
    })();
</script>
</body>
</html>

3. 代碼關鍵點解析

模塊                說明                                                                                                                                 

缺口生成         `gapX` 隨機生成,限制在畫布左右邊距內,避免缺口溢出。                                                                                

滑塊拖拽          使用獨立 `div` 模擬滑塊按鈕,監聽 `mousedown`/`touchstart` 開始拖拽,全局監聽移動事件。                                              

滑塊位置映射  將按鈕在軌道上的 `left` 值(0~`maxLeft`)線性映射到 Canvas 上的 X 坐標(2~`canvas.width-SLIDER_SIZE-2`),保證滑塊繪制位置與按鈕同步。 

實時繪制        拖拽過程中不斷調用 `drawSliderOnCanvas()`,在底板缺口上方繪制移動的滑塊,視覺反饋更真實。                                            

驗證邏輯       松開滑鼠時計算滑塊與缺口水平距離,若小於 `TOLERANCE` 則通過。驗證通過後滑塊變色鎖定,不可再拖。                                     

重置功能      重新生成缺口位置,滑塊歸位,恢復樣式。                                                                                               

 4. 常見問題與註意事項

(1). 移動端適配  

   - 已添加 `touchstart`、`touchmove`、`touchend` 事件,並設置 `passive: false` 以允許 `preventDefault()` 阻止滚動。  

   - Canvas 尺寸建議固定,避免高分屏模糊(可考慮 `window.devicePixelRatio` 適配,本例略)。

(2). 安全性缺陷  

   - 純前端驗證極易被繞過。真實場景必須將拖拽軌跡、最終位置等參數加密發送至服務端校驗,並增加時間戳、流水號等防重放機制。

(3). 用戶體驗  

   - 驗證失敗時滑塊歸位並提示誤差,可增加緩動動畫,提升友好度。  

   - 缺口形狀可設計為異形(如凸起/凹陷),增加破解難度。

(4). 性能優化  

   - 拖拽期間頻繁繪制 Canvas,低端設備可能卡頓。可考慮使用 `requestAnimationFrame` 節流。

(5). 擴展性  

   - 可替換背景為真實圖片,缺口位置自適應。  

   - 增加滑塊陰影、透明效果,使其更像“拼圖塊”。

 5. 總結

上述示例完整實現了基於 Canvas + 獨立滑塊按鈕的滑動拼圖驗證碼,涵蓋了隨機缺口、拖拽跟隨、對齊驗證、狀態重置等核心功能。開發者可直接使用此代碼作為原型,並在此基礎上增加服務端校驗、行為分析等安全加固,以滿足生產環境需求。

標籤: 驗證 缺口 滑動 滑塊 background 位置

AmupuCopyright Amupu.Z-Blog.Some Rights Reserved.