一、组件概述
简易验证码组件是一种用于区分人机、防止恶意请求的前端安全校验组件。
通过生成随机验证码并要求用户正确输入,有效降低接口被脚本、爬虫或暴力攻击的风险。
二、核心功能
- 🔢 随机验证码生成
支持数字 / 字母 / 数字+字母组合 - 🖼 图形化展示
以图片形式展示验证码,避免被直接读取 - 🔄 点击刷新
支持手动刷新验证码,提高安全性 - ✅ 输入校验
对用户输入进行比对校验(支持大小写忽略) - ⚡ 轻量易用
无第三方依赖,集成成本低
三、工作流程
- 组件初始化时生成一组随机验证码
- 将验证码渲染为图形(Canvas / SVG / 图片)
- 用户输入验证码内容
- 提交时进行比对校验
- 校验成功则继续业务流程,否则提示重新输入
四、使用场景
- 登录 / 注册页面
- 表单提交防刷
- 短信 / 邮箱验证码前置校验
- 重要操作(修改密码、资金操作等)
五、优势特点
- 🧩 实现简单,维护成本低
- 🚀 前端即可完成,响应速度快
- 🔒 有效抵御基础自动化攻击
- 🔧 可扩展性强(样式、复杂度可配置)
六、注意事项
- 简易验证码不能替代高安全验证手段
- 关键业务场景建议结合:
- 后端校验
- 行为验证码
- 限流、风控策略
七、组件代码
<template>
<view class="captcha-container" @click="refresh">
<canvas
:canvas-id="canvasId"
:style="{ width: width + 'px', height: height + 'px' }"
class="captcha-canvas"
></canvas>
<!-- 显示倒计时 -->
<!-- <view v-if="countdown > 0" class="countdown-overlay">
<text class="countdown-text">{{ countdown }}s</text>
</view> -->
</view>
</template>
<script setup lang="ts">
import { getCurrentInstance, nextTick, onMounted, onUnmounted, ref } from 'vue';
interface CaptchaInstance {
verify: (inputCode: string) => boolean;
refresh: () => void;
getCode: () => string;
}
const props = withDefaults(
defineProps<{
/** 宽度 */
width?: number;
/** 高度 */
height?: number;
/** 验证码长度 */
codeLength?: number;
/** 倒计时时长 */
countdownTime?: number;
/** 是否在 onMounted 时刷新 */
onMountedRefresh?: boolean;
}>(),
{
width: 120,
height: 38,
codeLength: 4,
countdownTime: 30,
onMountedRefresh: true,
}
);
/**
* canvasId
*/
const canvasId = ref<string>(`captcha-${Date.now()}-${Math.random()}`);
/**
* 验证码文本
*/
const captchaCode = ref<string>('');
/**
* 倒计时
*/
const countdown = ref<number>(0);
/**
* 倒计时定时器
*/
let countdownTimer: ReturnType<typeof setInterval> | null = null;
/**
* 生成随机字符
*/
function generateRandomChar(): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz123456789';
return chars[Math.floor(Math.random() * chars.length)];
}
/**
* 生成验证码文本
*/
function generateCode(): string {
let code = '';
for (let i = 0; i < props.codeLength; i++) {
code += generateRandomChar();
}
return code;
}
/**
* 绘制验证码
*/
function drawCaptcha(): void {
// 在 uni-app 中使用 uni.createCanvasContext
const ctx = uni.createCanvasContext(canvasId.value, getCurrentInstance());
const { width, height } = props;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 设置背景色
ctx.setFillStyle('#f8f9fa');
ctx.fillRect(0, 0, width, height);
// 生成新的验证码
captchaCode.value = generateCode();
// 绘制验证码文字
const fontSize = Math.floor(height * 0.6);
ctx.setFontSize(fontSize);
ctx.setTextBaseline('middle');
// 绘制每个字符
const charWidth = width / props.codeLength;
for (let i = 0; i < captchaCode.value.length; i++) {
// 随机颜色
const color = `rgb(${Math.floor(Math.random() * 150)}, ${Math.floor(
Math.random() * 150
)}, ${Math.floor(Math.random() * 150)})`;
ctx.setFillStyle(color);
// 随机旋转角度
const angle = (Math.random() - 0.5) * 0.4;
const x = charWidth * i + charWidth / 2;
const y = height / 2;
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillText(captchaCode.value[i], 0, 0);
ctx.restore();
}
// 绘制干扰线
for (let i = 0; i < 3; i++) {
ctx.setStrokeStyle(
`rgb(${Math.floor(Math.random() * 200)}, ${Math.floor(
Math.random() * 200
)}, ${Math.floor(Math.random() * 200)})`
);
ctx.setLineWidth(1);
ctx.beginPath();
ctx.moveTo(Math.random() * width, Math.random() * height);
ctx.lineTo(Math.random() * width, Math.random() * height);
ctx.stroke();
}
// 绘制干扰点
for (let i = 0; i < 30; i++) {
ctx.setFillStyle(
`rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(
Math.random() * 255
)}, ${Math.floor(Math.random() * 255)})`
);
ctx.beginPath();
ctx.arc(Math.random() * width, Math.random() * height, 1, 0, 2 * Math.PI);
ctx.fill();
}
ctx.draw();
}
/**
* 获取当前的验证码
*/
function getCode(): string {
return captchaCode.value;
}
/**
* 验证输入的验证码
*/
function verify(inputCode: string): boolean {
return inputCode.toLowerCase() === captchaCode.value.toLowerCase();
}
/**
* 启动倒计时
*/
function startCountdown(): void {
// 清除已有定时器
if (countdownTimer) {
clearInterval(countdownTimer);
}
countdown.value = props.countdownTime;
countdownTimer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(countdownTimer!);
countdownTimer = null;
// 倒计时结束,自动刷新验证码
refresh();
}
}, 1000);
}
/**
* 刷新验证码
*/
function refresh(): void {
drawCaptcha();
// 重新倒计时
startCountdown();
}
onMounted(async () => {
if (props.onMountedRefresh) {
await nextTick();
refresh();
}
});
onUnmounted(() => {
// 组件卸载时清除定时器
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
});
/**
* 暴露方法给父组件
*/
defineExpose<CaptchaInstance>({
getCode,
verify,
refresh,
});
</script>
<style lang="scss" scoped>
.captcha-container {
display: inline-block;
cursor: pointer;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
position: relative;
.captcha-canvas {
display: block;
}
.countdown-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
pointer-events: none;
.countdown-text {
color: rgba(255, 255, 255, 0.8);
font-size: 12px;
font-weight: bold;
}
}
}
</style>
八、应用示例
<template>
<view class="captcha-wrapper">
<u-input
v-model="form.captcha"
placeholder="请输入验证码"
class="captcha-input"
></u-input>
<x-captcha ref="captchaRef" class="captcha-component"></x-captcha>
</view>
</template>
import XCaptcha from 'src/components/x-captcha.vue';
const captchaRef = ref<InstanceType<typeof XCaptcha>>();
// 获取验证码值:captchaRef.value?.getCode();
// 校验是否一致:captchaRef.value?.verify(formData.value.captcha);
// 刷新验证码值:captchaRef.value?.refresh();
.captcha-wrapper {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
.captcha-input {
flex: 1;
}
.captcha-component {
flex-shrink: 0;
}
}