此示例使用 HTML + Canvas 實現滑動拼圖驗證碼。Canvas繪制帶隨機缺口的背景,獨立滑塊按鈕監聽滑鼠/觸摸拖拽,實時將滑塊位置映射到Ca...
如何使用HTML+JavaScript實現滑動驗證碼_前端滑動驗證碼
本文摘要
此示例使用 HTML + Canvas 實現滑動拼圖驗證碼。Canvas繪制帶隨機缺口的背景,獨立滑塊按鈕監聽滑鼠/觸摸拖拽,實時將滑塊位置映射到Canvas並繪制移動拼塊。松開時計算滑塊與缺口水平距離,在容差內即驗證通過,失敗則回彈。代碼包含重置功能,可刷新缺口位置。註意:此為前端演示,無服務端校驗,真實應用需加密傳輸軌跡並二次驗證。
以下是使用 HTML + JavaScript(Canvas) 實現滑動拼圖驗證碼的完整示例。該示例模擬了主流滑動驗證碼的核心流程:隨機生成缺口、拖拽滑塊、驗證對齊。
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 + 獨立滑塊按鈕的滑動拼圖驗證碼,涵蓋了隨機缺口、拖拽跟隨、對齊驗證、狀態重置等核心功能。開發者可直接使用此代碼作為原型,並在此基礎上增加服務端校驗、行為分析等安全加固,以滿足生產環境需求。
相關文章
