简易验证码组件

36 阅读3分钟

9a166238050362934bc01a29b014f319.png

一、组件概述

简易验证码组件是一种用于区分人机、防止恶意请求的前端安全校验组件。
通过生成随机验证码并要求用户正确输入,有效降低接口被脚本、爬虫或暴力攻击的风险。


二、核心功能

  • 🔢 随机验证码生成
    支持数字 / 字母 / 数字+字母组合
  • 🖼 图形化展示
    以图片形式展示验证码,避免被直接读取
  • 🔄 点击刷新
    支持手动刷新验证码,提高安全性
  • 输入校验
    对用户输入进行比对校验(支持大小写忽略)
  • 轻量易用
    无第三方依赖,集成成本低

三、工作流程

  1. 组件初始化时生成一组随机验证码
  2. 将验证码渲染为图形(Canvas / SVG / 图片)
  3. 用户输入验证码内容
  4. 提交时进行比对校验
  5. 校验成功则继续业务流程,否则提示重新输入

四、使用场景

  • 登录 / 注册页面
  • 表单提交防刷
  • 短信 / 邮箱验证码前置校验
  • 重要操作(修改密码、资金操作等)

五、优势特点

  • 🧩 实现简单,维护成本低
  • 🚀 前端即可完成,响应速度快
  • 🔒 有效抵御基础自动化攻击
  • 🔧 可扩展性强(样式、复杂度可配置)

六、注意事项

  • 简易验证码不能替代高安全验证手段
  • 关键业务场景建议结合:
    • 后端校验
    • 行为验证码
    • 限流、风控策略

七、组件代码

<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;
    }
}