【前端登录】实现一个带图形验证码的登录页组件(Vue3 + React)_前端图形验证码
🔐 带图形验证码的登录页组件实现(Vue3 + React)
【前端登录】业务方式全解析:从传统到现代,涵盖 Vue、React、Angular、jQuery 与原生 JS 最佳实践
我们将基于登录业务流程,封装一个 带图形验证码的完整登录页组件,分别用 Vue 3 和 React 实现。
🧠 功能需求与思路流程
✅ 核心功能
- 用户名/邮箱输入框
- 密码输入框
- 图形验证码生成与刷新
- 登录按钮
- 表单验证逻辑
- 提交处理
📦 技术选型
- Vue 3:使用 Composition API +
语法 - React:使用 Hooks(useState, useEffect)
- 验证码生成:HTML5 Canvas 绘制
下面老曹将 详细讲解该组件中每个核心功能模块的设计与算法思路,包括:
- 验证码生成算法
- 表单验证流程
- 数据绑定机制
- 事件响应逻辑
🔐 带图形验证码登录组件 —— 核心算法详解
🧠 一、整体结构概览
组件主要由以下部分构成:
🟢 二、图形验证码生成算法详解(关键步骤)
验证码绘制的核心是通过 HTML5 实现,使用随机字符 + 干扰元素增强安全性。
✅ 步骤 1:生成随机字符
function generateRandomChar() { const chars = \'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\'; return chars.charAt(Math.floor(Math.random() * chars.length));}
chars定义可用字符集- 使用
Math.random()从字符集中随机选择字符 - 返回单个字符用于拼接验证码字符串
示例输出:
\'A\',\'k\',\'3\'等
✅ 步骤 2:组合四位验证码
let captcha = \'\';for (let i = 0; i < 4; i++) { captcha += generateRandomChar();}
- 循环 4 次,生成一个 4 位验证码字符串
- 存储在
localStorage或组件状态中用于后续校验
示例输出:
\'aX9k\'
✅ 步骤 3:清空画布并设置背景色
ctx.clearRect(0, 0, canvas.width, canvas.height);ctx.fillStyle = \'#f0f0f0\';ctx.fillRect(0, 0, canvas.width, canvas.height);
- 清除画布内容,避免旧图像残留
- 设置背景颜色为浅灰色,提高可读性
✅ 步骤 4:添加干扰线(防识别)
for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height); ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height); ctx.strokeStyle = randomColor(150, 200); ctx.stroke();}
- 画 4 条随机位置的线段
- 颜色使用
randomColor函数生成偏暗的颜色,防止干扰文字辨识
✅ 步骤 5:添加干扰点(进一步防识别)
for (let i = 0; i < 30; i++) { ctx.beginPath(); ctx.arc( Math.random() * canvas.width, Math.random() * canvas.height, 1, 0, Math.PI * 2 ); ctx.fillStyle = randomColor(150, 200); ctx.fill();}
- 在画布上绘制 30 个随机位置的小圆点
- 用作视觉干扰,增加 OCR 识别难度
✅ 步骤 6:绘制验证码字符(含样式变换)
ctx.font = \'bold 20px Arial\';ctx.textAlign = \'center\';ctx.textBaseline = \'middle\';for (let i = 0; i < captchaText.length; i++) { ctx.fillStyle = randomColor(50, 150); ctx.save(); ctx.translate(20 + i * 20, 20); ctx.rotate((Math.random() * 20 - 10) * Math.PI / 180); ctx.fillText(captchaText[i], 0, 0); ctx.restore();}
- 对每个字符单独绘制
- 添加随机旋转角度(-10° ~ +10°)
- 设置不同颜色以增强视觉混淆
- 使用
save/restore保证每次变换不影响其他字符
✅ 步骤 7:保存验证码用于校验
localStorage.setItem(\'captcha\', captcha);
- 将当前验证码存储在浏览器本地
- 登录时比对用户输入与该值是否一致
🔵 三、表单验证逻辑详解
✅ 步骤 1:监听提交事件
handleSubmit() { // ...}
- 当用户点击“登录”按钮时触发
✅ 步骤 2:检查字段是否为空
if (!formData.username || !formData.password || !formData.captcha) { alert(\'请填写所有字段\'); return;}
- 判断用户名、密码、验证码是否都已填写
- 若任意为空,则提示并中断流程
✅ 步骤 3:比较验证码
const storedCaptcha = localStorage.getItem(\'captcha\');if (formData.captcha.toLowerCase() !== storedCaptcha?.toLowerCase()) { alert(\'验证码错误,请重试\'); generateCaptcha(); // 刷新验证码 return;}
- 获取之前生成的验证码
- 忽略大小写进行比对
- 如果不匹配,提示错误并刷新验证码
✅ 步骤 4:模拟登录成功
alert(\'登录成功!\');// 这里可以发送请求到后端进行登录验证
- 显示成功提示(真实项目中应调用 API)
- 可在此处跳转页面或设置登录状态
🟡 四、数据绑定与交互逻辑详解(Vue3/React)
✅ Vue3 中的数据绑定(Composition API)
const formData = reactive({ username: \'\', password: \'\', captcha: \'\'});
- 使用
reactive创建响应式对象 - 模板中通过
v-model自动绑定输入框值
✅ React 中的数据绑定(useState)
const [formData, setFormData] = useState({ username: \'\', password: \'\', captcha: \'\'});
- 使用
useState管理表单状态 - 输入框通过
onChange更新状态
✅ 输入框变更处理(React)
const handleChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value });};
- 使用动态键名更新对应字段
- 实现双向绑定效果
🟣 五、完整算法流程图解(逻辑顺序)
开始│├── 初始化│ └── 页面加载时调用 generateCaptcha()│├── 用户操作│ ├── 输入用户名│ ├── 输入密码│ ├── 输入验证码│ └── 点击刷新验证码图标重新生成│├── 提交登录│ ├── 检查字段是否为空│ ├── 检查验证码是否正确│ │ └── 不正确 → 提示并刷新验证码│ └── 验证通过 → 提示登录成功(或调用 API)│└── 结束
✅ 六、关键技术点总结
localStorage 临时保存用于校验reactive,React 使用 useState📌 七、扩展建议(进阶方向)
再给大家来个双版本的登录最佳实践! 👇
🟢 Vue 3 实现版本
📄 文件结构:
components/└── LoginWithCaptcha.vue
💻 完整代码:
<template> <div class=\"login-container\"> <div class=\"login-form\"> <h2>登录</h2> <div class=\"form-group\"> <label>用户名</label> <input v-model=\"formData.username\" type=\"text\" placeholder=\"请输入用户名\" /> </div> <div class=\"form-group\"> <label>密码</label> <input v-model=\"formData.password\" type=\"password\" placeholder=\"请输入密码\" /> </div> <div class=\"form-group captcha-row\"> <label>验证码</label> <div class=\"captcha-wrapper\"> <canvas ref=\"canvasRef\" width=\"100\" height=\"40\" @click=\"generateCaptcha\"></canvas> <span class=\"refresh-icon\" @click=\"generateCaptcha\">🔄</span> </div> <input v-model=\"formData.captcha\" type=\"text\" placeholder=\"输入验证码\" /> </div> <button class=\"login-btn\" @click=\"handleSubmit\">登录</button> </div> </div></template><script setup>import { ref, reactive, onMounted } from \'vue\';// 表单数据对象const formData = reactive({ username: \'\', password: \'\', captcha: \'\'});// canvas 元素引用const canvasRef = ref(null);// 生成随机验证码字符function generateRandomChar() { const chars = \'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\'; return chars.charAt(Math.floor(Math.random() * chars.length));}// 渲染验证码图像function drawCaptcha(captchaText) { const canvas = canvasRef.value; const ctx = canvas.getContext(\'2d\'); // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 设置背景颜色 ctx.fillStyle = \'#f0f0f0\'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 添加干扰线 for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height); ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height); ctx.strokeStyle = randomColor(150, 200); ctx.stroke(); } // 添加干扰点 for (let i = 0; i < 30; i++) { ctx.beginPath(); ctx.arc( Math.random() * canvas.width, Math.random() * canvas.height, 1, 0, Math.PI * 2 ); ctx.fillStyle = randomColor(150, 200); ctx.fill(); } // 绘制文字 ctx.font = \'bold 20px Arial\'; ctx.textAlign = \'center\'; ctx.textBaseline = \'middle\'; // 每个字符单独绘制并添加偏移和旋转 for (let i = 0; i < captchaText.length; i++) { ctx.fillStyle = randomColor(50, 150); ctx.save(); ctx.translate(20 + i * 20, 20); ctx.rotate((Math.random() * 20 - 10) * Math.PI / 180); ctx.fillText(captchaText[i], 0, 0); ctx.restore(); }}// 生成随机颜色function randomColor(min = 0, max = 255) { const r = Math.floor(Math.random() * (max - min + 1)) + min; const g = Math.floor(Math.random() * (max - min + 1)) + min; const b = Math.floor(Math.random() * (max - min + 1)) + min; return `rgb(${r},${g},${b})`;}// 生成新验证码function generateCaptcha() { let captcha = \'\'; for (let i = 0; i < 4; i++) { captcha += generateRandomChar(); } drawCaptcha(captcha); localStorage.setItem(\'captcha\', captcha); // 存储用于校验}// 表单提交处理function handleSubmit() { const storedCaptcha = localStorage.getItem(\'captcha\'); if (!formData.username || !formData.password || !formData.captcha) { alert(\'请填写所有字段\'); return; } if (formData.captcha.toLowerCase() !== storedCaptcha?.toLowerCase()) { alert(\'验证码错误,请重试\'); generateCaptcha(); return; } alert(\'登录成功!\'); // 这里可以发送请求到后端进行登录验证}// 页面加载时生成初始验证码onMounted(() => { generateCaptcha();});</script><style scoped>.login-container { display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f8f8f8;}.login-form { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); width: 300px;}.form-group { margin-bottom: 1rem;}.form-group label { display: block; margin-bottom: 0.5rem; font-weight: bold;}.form-group input { width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px;}.captcha-row .captcha-wrapper { display: flex; align-items: center; gap: 10px; margin-bottom: 0.5rem;}.refresh-icon { cursor: pointer; color: #007bff; font-size: 1.2em;}.login-btn { width: 100%; padding: 0.75rem; background-color: #007bff; color: white; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer;}.login-btn:hover { background-color: #0056b3;}</style>
🔵 React 实现版本
📄 文件结构:
components/└── LoginWithCaptcha.jsx
💻 完整代码:
import React, { useRef, useState, useEffect } from \'react\';function LoginWithCaptcha() { // 表单状态 const [formData, setFormData] = useState({ username: \'\', password: \'\', captcha: \'\' }); // canvas 引用 const canvasRef = useRef(null); // 生成随机字符 const generateRandomChar = () => { const chars = \'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\'; return chars.charAt(Math.floor(Math.random() * chars.length)); }; // 渲染验证码图像 const drawCaptcha = (captchaText) => { const canvas = canvasRef.current; const ctx = canvas.getContext(\'2d\'); // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 设置背景颜色 ctx.fillStyle = \'#f0f0f0\'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 添加干扰线 for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height); ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height); ctx.strokeStyle = randomColor(150, 200); ctx.stroke(); } // 添加干扰点 for (let i = 0; i < 30; i++) { ctx.beginPath(); ctx.arc( Math.random() * canvas.width, Math.random() * canvas.height, 1, 0, Math.PI * 2 ); ctx.fillStyle = randomColor(150, 200); ctx.fill(); } // 绘制文字 ctx.font = \'bold 20px Arial\'; ctx.textAlign = \'center\'; ctx.textBaseline = \'middle\'; // 每个字符单独绘制并添加偏移和旋转 for (let i = 0; i < captchaText.length; i++) { ctx.fillStyle = randomColor(50, 150); ctx.save(); ctx.translate(20 + i * 20, 20); ctx.rotate((Math.random() * 20 - 10) * Math.PI / 180); ctx.fillText(captchaText[i], 0, 0); ctx.restore(); } }; // 生成随机颜色 const randomColor = (min = 0, max = 255) => { const r = Math.floor(Math.random() * (max - min + 1)) + min; const g = Math.floor(Math.random() * (max - min + 1)) + min; const b = Math.floor(Math.random() * (max - min + 1)) + min; return `rgb(${r},${g},${b})`; }; // 生成新验证码 const generateCaptcha = () => { let captcha = \'\'; for (let i = 0; i < 4; i++) { captcha += generateRandomChar(); } drawCaptcha(captcha); localStorage.setItem(\'captcha\', captcha); // 存储用于校验 }; // 表单提交处理 const handleSubmit = (e) => { e.preventDefault(); const storedCaptcha = localStorage.getItem(\'captcha\'); if (!formData.username || !formData.password || !formData.captcha) { alert(\'请填写所有字段\'); return; } if (formData.captcha.toLowerCase() !== storedCaptcha?.toLowerCase()) { alert(\'验证码错误,请重试\'); generateCaptcha(); return; } alert(\'登录成功!\'); // 这里可以发送请求到后端进行登录验证 }; // 输入框变更处理 const handleChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; // 页面加载时生成初始验证码 useEffect(() => { generateCaptcha(); }, []); return ( <div className=\"login-container\"> <div className=\"login-form\"> <h2>登录</h2> <form onSubmit={handleSubmit}> {/* 用户名 */} <div className=\"form-group\"> <label>用户名</label> <input name=\"username\" value={formData.username} onChange={handleChange} type=\"text\" placeholder=\"请输入用户名\" /> </div> {/* 密码 */} <div className=\"form-group\"> <label>密码</label> <input name=\"password\" value={formData.password} onChange={handleChange} type=\"password\" placeholder=\"请输入密码\" /> </div> {/* 验证码 */} <div className=\"form-group captcha-row\"> <label>验证码</label> <div className=\"captcha-wrapper\"> <canvas ref={canvasRef} width={100} height={40} onClick={generateCaptcha} /> <span className=\"refresh-icon\" onClick={generateCaptcha}>🔄</span> </div> <input name=\"captcha\" value={formData.captcha} onChange={handleChange} type=\"text\" placeholder=\"输入验证码\" /> </div> {/* 登录按钮 */} <button className=\"login-btn\" type=\"submit\">登录</button> </form> </div> </div> );}export default LoginWithCaptcha;
🎨 样式文件(App.css 或组件内样式):
.login-container { display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f8f8f8;}.login-form { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); width: 300px;}.form-group { margin-bottom: 1rem;}.form-group label { display: block; margin-bottom: 0.5rem; font-weight: bold;}.form-group input { width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px;}.captcha-row .captcha-wrapper { display: flex; align-items: center; gap: 10px; margin-bottom: 0.5rem;}.refresh-icon { cursor: pointer; color: #007bff; font-size: 1.2em;}.login-btn { width: 100%; padding: 0.75rem; background-color: #007bff; color: white; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer;}.login-btn:hover { background-color: #0056b3;}
✅ 总结
ref, reactiveuseStateonMounteduseEffectrefuseRef@click 等指令onClick 等属性v-modelonChange你可以将上述任意一种实现方式集成到你的项目中,打造一个功能完整的登录页。


