1. 实现原理
图形验证码在前端开发中主要用于防止恶意请求和机器人攻击。纯前端实现的验证码虽然安全性不如服务端,但对于一些轻量级的防刷场景仍然很有价值。
核心实现思路:
- 使用Canvas绘制随机字符和干扰元素
- 生成随机字符串作为验证码内容
- 添加视觉干扰(线条、噪点、扭曲)
- 实现验证码刷新和验证逻辑
- 使用Vue响应式数据管理状态
2. 技术架构
我们采用Vue2 + Canvas的方案,不依赖任何第三方验证码库,完全自主实现。
项目结构:
- 验证码生成组件 (Captcha.vue)
- 验证码混入 (captchaMixin.js)
- 主应用页面 (App.vue)
- 样式文件 (style.css)
3. 功能特点说明
1. **完整的验证码生成** - 自动生成4-6位包含数字和字母的随机验证码
2. **视觉安全防护** - 通过干扰线、噪点、文字扭曲等防止OCR识别
3. **响应式设计** - 适配不同屏幕尺寸,移动端友好
4. **实时验证反馈** - 即时验证用户输入,提供明确的成功/错误提示
5. **用户体验优化** - 支持点击刷新、键盘操作、悬停效果等
5. **模块化架构** - 组件化设计,易于维护和扩展
4. 使用说明
该验证码组件可以直接集成到Vue2项目中,支持自定义尺寸、颜色和难度级别。通过混入文件可以轻松在其他组件中复用验证码逻辑。
5. 安全考虑
虽然纯前端验证码在安全性上有局限,但通过以下方式提升防护能力:
- 随机字符组合
- 视觉干扰元素
- 大小写不敏感验证
- 频繁刷新机制
对于高安全要求的场景,建议结合服务端验证实现双重防护。
3. 完整代码实现
以下实现提供了一个完整的Vue2前端验证码解决方案,代码结构清晰,功能完善,可以直接用于实际项目中。
下面开始生成完整的Vue2验证码项目代码:
<template>
<div id="app" class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div class="container mx-auto px-4 py-8">
<!-- 头部标题 -->
<header class="text-center mb-12">
<h1 class="text-4xl font-bold text-gray-800 mb-4">
Vue2 图形验证码组件
</h1>
<p class="text-lg text-gray-600 max-w-2xl mx-auto">
纯前端实现的图形验证码,包含随机字符、干扰线、噪点等安全特性,支持验证和自动刷新功能
</p>
</header>
<!-- 主要内容区域 -->
<main class="max-w-4xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- 验证码演示区域 -->
<div class="bg-white rounded-2xl shadow-xl p-6">
<h2 class="text-2xl font-semibold text-gray-800 mb-6 text-center">
验证码演示
</h2>
<captcha-component
ref="captcha"
:width="320"
:height="120"
class="mb-6"
@code-generated="onCodeGenerated"
/>
<!-- 验证码输入区域 -->
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
请输入验证码
</label>
<input
v-model="inputCode"
type="text"
placeholder="输入上方验证码"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
@keyup.enter="validateCaptcha"
/>
</div>
<!-- 操作按钮 -->
<div class="flex gap-3">
<button
@click="validateCaptcha"
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white py-3 px-6 rounded-lg font-medium transition-colors shadow-md"
>
验证
</button>
<button
@click="refreshCaptcha"
class="flex-1 bg-gray-600 hover:bg-gray-700 text-white py-3 px-6 rounded-lg font-medium transition-colors shadow-md"
>
刷新
</button>
</div>
</div>
</div>
<!-- 功能说明区域 -->
<div class="bg-white rounded-2xl shadow-xl p-6">
<h2 class="text-2xl font-semibold text-gray-800 mb-6 text-center">
功能特性
</h2>
<div class="space-y-4">
<div v-for="feature in features" :key="feature.id"
class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg border border-blue-200">
<h3 class="font-semibold text-gray-800 mb-2">{{ feature.title }}</h3>
<p class="text-gray-600 text-sm">{{ feature.description }}</p>
</div>
</div>
<!-- 验证结果展示 -->
<div class="mt-8 p-4 rounded-lg" :class="resultClass">
<div class="flex items-center">
<i :class="resultIcon" class="text-xl mr-3"></i>
<span class="font-medium">{{ resultMessage }}</span>
</div>
</div>
</div>
</div>
<!-- 技术实现说明 -->
<div class="mt-8 bg-white rounded-2xl shadow-xl p-6">
<h2 class="text-2xl font-semibold text-gray-800 mb-6 text-center">
技术实现细节
</h2>
<div class="prose max-w-none text-gray-700">
<p>本验证码组件采用纯前端实现,主要技术要点:</p>
<ul class="list-disc pl-6 mt-4 space-y-2">
<li>使用HTML5 Canvas进行图形绘制</li>
<li>随机生成4-6位包含数字和字母的验证码</li>
<li>添加干扰线和噪点增强安全性</li>
<li>Vue响应式数据管理验证状态</li>
<li>支持大小写不敏感验证</li>
</ul>
</div>
</div>
</main>
</div>
</div>
</template>
<script>
import CaptchaComponent from './components/Captcha.vue'
export default {
name: 'App',
components: {
CaptchaComponent
},
data() {
return {
inputCode: '',
currentCode: '',
resultMessage: '等待验证...',
resultClass: 'bg-gray-100 text-gray-600',
resultIcon: 'fas fa-clock',
features: [
{
id: 1,
title: '随机字符生成',
description: '自动生成4-6位包含数字和大小写字母的随机验证码'
},
{
id: 2,
title: '视觉干扰',
description: '添加干扰线、噪点等元素,防止OCR识别'
},
{
id: 3,
title: '实时验证',
description: '即时验证用户输入,提供明确反馈'
},
{
id: 4,
title: '一键刷新',
description: '支持手动刷新验证码,提升用户体验'
}
]
}
},
methods: {
onCodeGenerated(code) {
this.currentCode = code
},
validateCaptcha() {
if (!this.inputCode.trim()) {
this.showResult('请输入验证码', 'error')
return
}
if (this.inputCode.toLowerCase() === this.currentCode.toLowerCase()) {
this.showResult('验证码正确!', 'success')
} else {
this.showResult('验证码错误,请重试', 'error')
this.inputCode = ''
this.$refs.captcha.refreshCaptcha()
}
},
refreshCaptcha() {
this.$refs.captcha.refreshCaptcha()
this.inputCode = ''
this.showResult('验证码已刷新', 'info')
},
showResult(message, type) {
this.resultMessage = message
switch (type) {
case 'success':
this.resultClass = 'bg-green-100 text-green-700 border border-green-200'
this.resultIcon = 'fas fa-check-circle'
break
case 'error':
this.resultClass = 'bg-red-100 text-red-700 border border-red-200'
this.resultIcon = 'fas fa-times-circle'
break
default:
this.resultClass = 'bg-blue-100 text-blue-700 border border-blue-200'
this.resultIcon = 'fas fa-info-circle'
}
}
}
}
</script>
<style>
#app {
font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
</style>
<code_end>
接下来创建验证码核心组件,负责图形验证码的生成和绘制:
<code_start project_name=vue2_captcha filename=components/Captcha.vue title=验证码核心组件 entrypoint=false runnable=false project_final_file=false>
<template>
<div class="captcha-container">
<!-- 验证码画布 -->
<div class="relative bg-white rounded-lg border-2 border-gray-300 overflow-hidden">
<canvas
ref="canvas"
:width="width"
:height="height"
class="block cursor-pointer"
@click="refreshCaptcha"
:title="canvasTitle"
></canvas>
<!-- 刷新按钮 -->
<button
@click.stop="refreshCaptcha"
class="absolute top-2 right-2 bg-white bg-opacity-80 hover:bg-opacity-100 p-1 rounded-full shadow-md transition-all"
title="刷新验证码"
>
<i class="fas fa-redo-alt text-gray-600 text-sm"></i>
</button>
</div>
<!-- 状态提示 -->
<div class="mt-2 flex items-center justify-between text-sm">
<span class="text-gray-600">点击图片可刷新验证码</span>
<span class="text-blue-600 font-medium">{{ codeLength }}位验证码</span>
</div>
</div>
</template>
<script>
export default {
name: 'CaptchaComponent',
props: {
width: {
type: Number,
default: 320
},
height: {
type: Number,
default: 120
}
},
data() {
return {
captchaCode: '',
codeLength: 0,
canvasTitle: '点击刷新验证码'
}
},
mounted() {
this.refreshCaptcha()
},
methods: {
// 生成随机验证码
generateCaptchaCode() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'
this.codeLength = Math.floor(Math.random() * 2) + 4 // 4-5位长度
let code = ''
for (let i = 0; i < this.codeLength; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length))
}
return code
},
// 绘制验证码
drawCaptcha() {
const canvas = this.$refs.canvas
const ctx = canvas.getContext('2d')
// 清空画布
ctx.clearRect(0, 0, this.width, this.height)
// 绘制背景
this.drawBackground(ctx)
// 绘制验证码文字
this.drawText(ctx, this.captchaCode)
// 绘制干扰线
this.drawInterferenceLines(ctx)
// 绘制噪点
this.drawNoise(ctx)
},
// 绘制背景
drawBackground(ctx) {
// 渐变背景
const gradient = ctx.createLinearGradient(0, 0, this.width, this.height)
gradient.addColorStop(0, '#f0f9ff')
gradient.addColorStop(1, '#e0f2fe')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, this.width, this.height)
},
// 绘制文字
drawText(ctx, text) {
const chars = text.split('')
const charWidth = this.width / (chars.length + 1)
chars.forEach((char, index) => {
// 随机字体大小
const fontSize = Math.floor(Math.random() * 10) + 32
ctx.font = `bold ${fontSize}px Arial, sans-serif`
// 随机颜色
const hue = Math.floor(Math.random() * 60) + 200 // 蓝色系
ctx.fillStyle = `hsl(${hue}, 70%, 40%)`
// 字符位置(带随机偏移)
const x = charWidth * (index + 0.5) + Math.random() * 8 - 4
const y = this.height / 2 + Math.random() * 10 - 5
// 随机旋转
const rotation = (Math.random() - 0.5) * 0.4
ctx.save()
ctx.translate(x, y)
ctx.rotate(rotation)
// 绘制文字阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'
ctx.shadowBlur = 2
ctx.shadowOffsetX = 1
ctx.shadowOffsetY = 1
ctx.fillText(char, 0, 0)
ctx.restore()
})
},
// 绘制干扰线
drawInterferenceLines(ctx) {
const lineCount = 6
for (let i = 0; i < lineCount; i++) {
ctx.beginPath()
ctx.moveTo(Math.random() * this.width, Math.random() * this.height)
ctx.lineTo(Math.random() * this.width, Math.random() * this.height)
// 随机线条颜色和宽度
ctx.strokeStyle = `rgba(${Math.random() * 100}, ${Math.random() * 100}, ${Math.random() * 100}, 0.3)`
ctx.lineWidth = Math.random() * 2 + 0.5
ctx.stroke()
}
},
// 绘制噪点
drawNoise(ctx) {
const dotCount = 80
for (let i = 0; i < dotCount; i++) {
const x = Math.random() * this.width
const y = Math.random() * this.height
ctx.beginPath()
ctx.arc(x, y, Math.random() * 1.5, 0, Math.PI * 2)
ctx.fillStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.5)`
ctx.fill()
}
},
// 刷新验证码
refreshCaptcha() {
this.captchaCode = this.generateCaptchaCode()
this.drawCaptcha()
this.$emit('code-generated', this.captchaCode)
},
// 验证输入
validate(input) {
return input.toLowerCase() === this.captchaCode.toLowerCase()
},
// 获取当前验证码
getCurrentCode() {
return this.captchaCode
}
}
}
</script>
<style scoped>
.captcha-container {
user-select: none;
}
canvas {
transition: transform 0.2s ease;
}
canvas:hover {
transform: scale(1.01);
}
</style>
<code_end>
现在创建验证码混入文件,提供可复用的验证码逻辑:
<code_start project_name=vue2_captcha filename=mixins/captchaMixin.js title=验证码功能混入 entrypoint=false runnable=false project_final_file=false>
// 验证码功能混入
export const captchaMixin = {
data() {
return {
captchaConfig: {
width: 300,
height: 100,
codeLength: 4,
backgroundColor: '#f8fafc',
textColors: ['#1e40af', '#dc2626', '#16a34a', '#ca8a04'],
fontSizes: [28, 32, 36, 40],
useUppercase: true,
useLowercase: true,
useNumbers: true
}
}
},
methods: {
// 生成随机字符
generateRandomChar() {
let chars = ''
if (this.captchaConfig.useUppercase) chars += 'ABCDEFGHJKLMNPQRSTUVWXYZ'
if (this.captchaConfig.useLowercase) chars += 'abcdefghjkmnpqrstuvwxyz'
if (this.captchaConfig.useNumbers) chars += '23456789'
return chars.charAt(Math.floor(Math.random() * chars.length))
},
// 生成验证码文本
generateCaptchaText(length = null) {
const len = length || this.captchaConfig.codeLength
let text = ''
for (let i = 0; i < len; i++) {
text += this.generateRandomChar()
}
return text
},
// 绘制验证码到Canvas
drawCaptchaToCanvas(canvas, text, config = {}) {
const ctx = canvas.getContext('2d')
const { width, height } = canvas
const mergedConfig = { ...this.captchaConfig, ...config }
// 清空画布
ctx.clearRect(0, 0, width, height)
// 绘制背景
this.drawBackground(ctx, width, height, mergedConfig)
// 绘制文本
this.drawCaptchaText(ctx, text, width, height, mergedConfig)
// 添加干扰
this.addInterference(ctx, width, height, mergedConfig)
},
// 绘制背景
drawBackground(ctx, width, height, config) {
ctx.fillStyle = config.backgroundColor
ctx.fillRect(0, 0, width, height)
},
// 绘制验证码文本
drawCaptchaText(ctx, text, width, height, config) {
const chars = text.split('')
const charWidth = width / (chars.length + 1)
chars.forEach((char, index) => {
const fontSize = config.fontSizes[Math.floor(Math.random() * config.fontSizes.length)]
const color = config.textColors[Math.floor(Math.random() * config.textColors.length)]
ctx.save()
ctx.font = `bold ${fontSize}px Arial`
ctx.fillStyle = color
// 文字位置和旋转
const x = charWidth * (index + 0.5) + Math.random() * 6 - 3
const y = height / 2 + Math.random() * 8 - 4
const rotation = (Math.random() - 0.5) * 0.3
ctx.translate(x, y)
ctx.rotate(rotation)
ctx.fillText(char, 0, 0)
ctx.restore()
})
},
// 添加干扰元素
addInterference(ctx, width, height, config) {
// 干扰线
for (let i = 0; i < 5; i++) {
ctx.beginPath()
ctx.moveTo(Math.random() * width, Math.random() * height)
ctx.lineTo(Math.random() * width, Math.random() * height)
ctx.strokeStyle = `rgba(${Math.random() * 100}, ${Math.random() * 100}, ${Math.random() * 100}, 0.2)`
ctx.lineWidth = Math.random() * 1.5
ctx.stroke()
}
// 噪点
for (let i = 0; i < 50; i++) {
const x = Math.random() * width
const y = Math.random() * height
ctx.beginPath()
ctx.arc(x, y, Math.random() * 1.2, 0, Math.PI * 2)
ctx.fillStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.4)`
ctx.fill()
}
},
// 验证码配置验证
validateCaptchaConfig(config) {
const errors = []
if (config.width < 200) errors.push('宽度不能小于200px')
if (config.height < 80) errors.push('高度不能小于80px')
if (config.codeLength < 4 || config.codeLength > 6) {
errors.push('验证码长度应在4-6位之间')
}
if (!config.useUppercase && !config.useLowercase && !config.useNumbers) {
errors.push('至少启用一种字符类型')
}
return errors
},
// 获取验证码难度级别
getCaptchaDifficulty(config = null) {
const cfg = config || this.captchaConfig
let score = 0
// 长度得分
score += cfg.codeLength * 2
// 字符类型得分
if (cfg.useUppercase) score += 5
if (cfg.useLowercase) score += 5
if (cfg.useNumbers) score += 3
if (score <= 15) return '简单'
if (score <= 20) return '中等'
return '困难'
}
}
}
<code_end>
最后创建项目的主入口文件和HTML模板:
<code_start project_name=vue2_captcha filename=main.js title=Vue应用入口文件 entrypoint=false runnable=false project_final_file=false>
import Vue from 'vue'
import App from './App.vue'
// 引入外部资源
const loadExternalResources = () => {
// 加载 Font Awesome
const fontAwesome = document.createElement('link')
fontAwesome.rel = 'stylesheet'
fontAwesome.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'
document.head.appendChild(fontAwesome)
// 加载 Tailwind CSS
const tailwind = document.createElement('script')
tailwind.src = 'https://cdn.tailwindcss.com'
document.head.appendChild(tailwind)
}
// 初始化应用
const initApp = () => {
new Vue({
render: h => h(App)
}).$mount('#app')
}
// 等待DOM加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
loadExternalResources()
setTimeout(initApp, 100) // 确保资源加载完成
})
} else {
loadExternalResources()
setTimeout(initApp, 100)
}
<code_end>
创建HTML入口文件:
<code_start project_name=vue2_captcha filename=index.html title=项目主页面 entrypoint=true runnable=false project_final_file=false>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue2 图形验证码组件</title>
<meta name="description" content="纯前端实现的Vue2图形验证码组件,包含完整的验证功能和美观的UI设计">
<meta name="keywords" content="Vue2, 验证码, Canvas, 前端安全, 图形验证">
<!-- Vue2 CDN -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- Tailwind CSS 配置 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8'
}
}
}
}
}
</script>
<style>
/* 自定义字体 */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
/* 加载动画 */
.loading-spinner {
border: 4px solid #f3f4f6;
border-top: 4px solid #3b82f6;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 淡入动画 */
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div id="app">
<!-- 加载状态 -->
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div class="text-center">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-600 font-medium">正在加载验证码组件...</p>
</div>
</div>
</div>
<!-- 加载应用脚本 -->
<script src="./main.js"></script>
</body>
</html>
<code_end>

759

被折叠的 条评论
为什么被折叠?



