基于Hutool的验证码功能完整技术文档

📋 目录

  1. 功能概述
  2. 技术架构
  3. 环境依赖
  4. 代码结构分析
  5. 后端实现详解
  6. 前端实现详解
  7. 完整实现步骤
  8. 配置说明
  9. API接口文档
  10. 常见问题解决
  11. 扩展建议

🎯 功能概述

本项目实现了一个完整的图形验证码功能,具备以下特性:

核心功能

  • 验证码生成:使用Hutool工具类生成图形验证码
  • 验证码验证:支持用户输入验证,验证后自动失效
  • 验证码刷新:支持手动刷新获取新验证码
  • 自定义参数:支持自定义验证码宽度、高度、字符数、干扰线数量

技术特点

  • 安全性:验证码ID与答案分离存储,前端无法获取答案
  • 时效性:验证码具有过期时间(默认5分钟)
  • 易用性:前端组件化,开箱即用
  • 响应式:支持移动端和PC端适配

🏗️ 技术架构

┌─────────────────┐    HTTP请求    ┌─────────────────┐
│   前端 Vue3     │ ──────────────→ │   后端 Spring   │
│                 │                 │                 │
│ - 验证码组件    │ ←────────────── │ - Controller    │
│ - 登录页面      │   JSON响应      │ - Service       │
│ - 状态管理      │                 │ - Cache         │
└─────────────────┘                 └─────────────────┘
         │                                   │
         │                                   │
         ▼                                   ▼
┌─────────────────┐                 ┌─────────────────┐
│  浏览器存储     │                 │   内存缓存      │
│                 │                 │                 │
│ - 验证码图片    │                 │ - 验证码答案    │
│ - 验证码ID      │                 │ - 过期时间      │
└─────────────────┘                 └─────────────────┘

核心组件说明

  • 前端组件:Vue3 + Composition API + Tailwind CSS
  • 后端服务:Spring Boot + Hutool工具类
  • 缓存机制:内存缓存(可扩展为Redis)
  • 数据传输:RESTful API + JSON格式

🛠️ 环境依赖

后端依赖

<!-- Spring Boot Starter Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Hutool工具类 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.22</version>
</dependency>

<!-- 日志依赖 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
</dependency>

前端依赖

{
  "vue": "^3.3.0",
  "vue-router": "^4.2.0",
  "tailwindcss": "^3.3.0",
  "naive-ui": "^2.34.0"
}

📁 代码结构分析

后端代码结构

cn.jbolt.admin.user/
├── controller/
│   └── CaptchaController.java      # 验证码控制器
├── service/
│   ├── CaptchaService.java         # 验证码服务接口
│   └── impl/
│       └── CaptchaServiceImpl.java # 验证码服务实现
└── util/
    └── cache/
        └── CaptchaCache.java       # 验证码缓存工具类

前端代码结构

src/
├── components/
│   └── verificationCode.vue       # 验证码组件
├── views/
│   └── login/
│       └── index.vue              # 登录页面
└── stores/
    └── user.js                    # 用户状态管理

⚙️ 后端实现详解

1. 控制器层(CaptchaController.java)

@RestController
@RequestMapping("/captcha")
@Uncheck  // 免登录验证注解
public class CaptchaController {
    
    @Autowired
    private CaptchaService captchaService;
    
    // 生成验证码(使用默认参数)
    @GetMapping("/generate")
    public Result generateCaptcha() {
        return captchaService.generateCaptcha();
    }
    
    // 生成自定义验证码
    @GetMapping("/generate/custom")
    public Result generateCustomCaptcha(
        @RequestParam(defaultValue = "200") int width,
        @RequestParam(defaultValue = "100") int height,
        @RequestParam(defaultValue = "4") int codeCount,
        @RequestParam(defaultValue = "20") int lineCount) {
        return captchaService.generateCaptcha(width, height, codeCount, lineCount);
    }
    
    // 验证验证码
    @PostMapping("/verify")
    public Result verifyCaptcha(
        @RequestParam String captchaId, 
        @RequestParam String code) {
        return captchaService.verifyCaptcha(captchaId, code);
    }
    
    // 刷新验证码
    @GetMapping("/refresh")
    public Result refreshCaptcha() {
        return captchaService.refreshCaptcha();
    }
}
关键点说明:
  • @Uncheck:确保验证码接口无需登录即可访问
  • 参数验证:使用@RequestParam设置默认值
  • 统一返回:使用Result包装返回结果

2. 服务层(CaptchaServiceImpl.java)

@Service
public class CaptchaServiceImpl implements CaptchaService {
    
    private static final Logger logger = LoggerFactory.getLogger(CaptchaServiceImpl.class);
    
    // 默认配置常量
    private static final int DEFAULT_WIDTH = 200;
    private static final int DEFAULT_HEIGHT = 100;
    private static final int DEFAULT_CODE_COUNT = 4;
    private static final int DEFAULT_LINE_COUNT = 20;
    private static final int DEFAULT_EXPIRE_MINUTES = 5;
    
    @Override
    public Result generateCaptcha(int width, int height, int codeCount, int lineCount) {
        try {
            // 1. 使用Hutool生成验证码
            LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(
                width, height, codeCount, lineCount);
            
            // 2. 生成唯一ID
            String captchaId = IdUtil.simpleUUID();
            
            // 3. 获取验证码答案和图片
            String code = lineCaptcha.getCode();
            String imageBase64 = lineCaptcha.getImageBase64Data();
            
            // 4. 存入缓存
            boolean cached = CaptchaCache.putCaptcha(
                captchaId, code, DEFAULT_EXPIRE_MINUTES, TimeUnit.MINUTES);
            
            if (!cached) {
                logger.error("验证码缓存失败: captchaId={}", captchaId);
                return Result.error("验证码生成失败,请重试");
            }
            
            // 5. 构建返回结果(不包含答案)
            Map<String, Object> result = new HashMap<>();
            result.put("captchaId", captchaId);
            result.put("captchaImage", imageBase64);
            
            logger.info("验证码生成成功: captchaId={}", captchaId);
            return Result.success(result, "验证码生成成功");
            
        } catch (Exception e) {
            logger.error("生成验证码失败", e);
            return Result.error("验证码生成失败,请重试");
        }
    }
    
    @Override
    public Result verifyCaptcha(String captchaId, String inputCode) {
        // 1. 参数校验
        if (StrUtil.isBlank(captchaId)) {
            return Result.error("验证码ID不能为空");
        }
        if (StrUtil.isBlank(inputCode)) {
            return Result.error("验证码不能为空");
        }
        
        try {
            // 2. 验证验证码(验证成功后自动移除)
            boolean isValid = CaptchaCache.verifyCaptcha(
                captchaId, inputCode.trim(), true);
            
            if (isValid) {
                logger.info("验证码验证成功: captchaId={}", captchaId);
                return Result.success(true, "验证码验证成功");
            } else {
                logger.warn("验证码验证失败: captchaId={}, inputCode={}", 
                    captchaId, inputCode);
                return Result.error("验证码错误或已过期");
            }
            
        } catch (Exception e) {
            logger.error("验证验证码时发生异常: captchaId={}, inputCode={}", 
                captchaId, inputCode, e);
            return Result.error("验证码验证失败,请重试");
        }
    }
}
核心技术点:
  1. Hutool验证码生成CaptchaUtil.createLineCaptcha()
  2. UUID生成IdUtil.simpleUUID()确保ID唯一性
  3. Base64编码getImageBase64Data()获取图片数据
  4. 缓存管理:自定义缓存类管理验证码生命周期
  5. 安全设计:返回结果不包含验证码答案

3. 缓存管理(CaptchaCache.java)

public class CaptchaCache {
    
    private static final Map<String, CaptchaInfo> CACHE = new ConcurrentHashMap<>();
    
    // 验证码信息内部类
    private static class CaptchaInfo {
        private final String code;
        private final long expireTime;
        
        public CaptchaInfo(String code, long expireTime) {
            this.code = code;
            this.expireTime = expireTime;
        }
        
        public boolean isExpired() {
            return System.currentTimeMillis() > expireTime;
        }
    }
    
    // 存储验证码
    public static boolean putCaptcha(String captchaId, String code, 
                                   int timeout, TimeUnit timeUnit) {
        long expireTime = System.currentTimeMillis() + timeUnit.toMillis(timeout);
        CACHE.put(captchaId, new CaptchaInfo(code, expireTime));
        return true;
    }
    
    // 验证验证码
    public static boolean verifyCaptcha(String captchaId, String inputCode, 
                                      boolean removeAfterVerify) {
        CaptchaInfo info = CACHE.get(captchaId);
        
        if (info == null || info.isExpired()) {
            CACHE.remove(captchaId); // 清理过期数据
            return false;
        }
        
        boolean isValid = info.code.equalsIgnoreCase(inputCode);
        
        if (removeAfterVerify) {
            CACHE.remove(captchaId); // 验证后移除
        }
        
        return isValid;
    }
}

🎨 前端实现详解

1. 验证码组件(verificationCode.vue)

<template>
    <div>
        <div class="flex space-x-4">
            <!-- 验证码输入框 -->
            <div class="flex-1">
                <div class="relative">
                    <!-- 图标 -->
                    <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none z-10">
                        <svg class="h-5 w-5" :class="iconClass" fill="currentColor">
                            <!-- SVG路径 -->
                        </svg>
                    </div>
                    
                    <!-- 输入框 -->
                    <input
                        id="captcha"
                        :value="modelValue"
                        @input="$emit('update:modelValue', $event.target.value)"
                        type="text"
                        class="input-class"
                        :class="inputClass"
                        placeholder="验证码"
                        @focus="onFocus"
                        @blur="onBlur"
                        @mouseenter="onMouseEnter"
                        @mouseleave="onMouseLeave"
                    >
                </div>
            </div>

            <!-- 验证码图片显示区域 -->
            <div
                class="w-32 h-12 relative overflow-hidden rounded-lg cursor-pointer"
                @click="refreshCaptcha"
                :class="{ 'animate-pulse': loading || isFocused }"
            >
                <!-- 加载状态 -->
                <div v-if="loading" class="loading-spinner">
                    <svg class="animate-spin h-6 w-6 text-gray-400">
                        <!-- 加载动画SVG -->
                    </svg>
                </div>

                <!-- 验证码图片 -->
                <img
                    v-else-if="captchaImage"
                    :src="captchaImage"
                    alt="验证码"
                    class="absolute inset-0 w-full h-full object-cover"
                    @error="onImageError"
                />

                <!-- 默认占位符 -->
                <div v-else class="placeholder-content">
                    点击获取
                </div>

                <!-- 刷新提示 -->
                <div class="refresh-overlay">
                    点击刷新
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import { computed } from 'vue';

export default {
    name: 'CaptchaComponent',
    props: {
        modelValue: String,      // 输入值
        error: String,           // 错误信息
        captchaImage: String,    // 验证码图片
        loading: Boolean,        // 加载状态
        focusField: String,      // 焦点字段
        hoverField: String       // 悬停字段
    },
    emits: ['update:modelValue', 'refresh', 'focus', 'blur', 'mouseenter', 'mouseleave'],
    
    setup(props, { emit }) {
        // 计算属性
        const isFocused = computed(() => props.focusField === 'captcha');
        const isHovered = computed(() => props.hoverField === 'captcha');
        
        const iconClass = computed(() => [
            props.error ? 'text-red-500' : 
            (isFocused.value ? 'text-green-500' : 'text-gray-400')
        ]);
        
        const inputClass = computed(() => [
            props.error ? 'ring-red-500 focus:ring-red-500' :
            (isFocused.value ? 'shadow-lg ring-[#4ab27d] ring-2' :
            (isHovered.value ? 'ring-[#4ab27d]/50' : 'ring-gray-200/70'))
        ]);
        
        // 事件处理
        const onFocus = () => emit('focus');
        const onBlur = () => emit('blur');
        const onMouseEnter = () => emit('mouseenter');
        const onMouseLeave = () => emit('mouseleave');
        const refreshCaptcha = () => emit('refresh');
        
        const onImageError = () => {
            console.warn('验证码图片加载失败');
            emit('refresh'); // 自动重新获取
        };

        return {
            isFocused,
            isHovered,
            iconClass,
            inputClass,
            onFocus,
            onBlur,
            onMouseEnter,
            onMouseLeave,
            refreshCaptcha,
            onImageError
        };
    }
}
</script>
组件特点:
  1. 响应式设计:支持焦点、悬停状态变化
  2. 错误处理:自动重试加载失败的验证码
  3. 用户体验:点击图片刷新,加载动画提示
  4. 样式动态:根据状态动态调整样式类

2. 登录页面集成(index.vue)

<script setup>
import { ref, reactive, onMounted, watch } from 'vue';
import { JBoltApi } from "@/service/request/index.js";
import CaptchaComponent from './cnps/verificationCode.vue';

// 验证码相关状态
const captcha = reactive({
    id: '',           // 验证码ID
    image: '',        // 验证码图片Base64
    loading: false    // 验证码加载状态
});

// 系统配置
const config = reactive({
    validateCodeEnable: false,  // 是否启用验证码
});

// 获取验证码
function loadCaptcha() {
    if (!config.validateCodeEnable) return;

    captcha.loading = true;

    JBoltApi.tryGet('/captcha/generate')
        .then((res) => {
            captcha.id = res.data.captchaId;
            captcha.image = res.data.captchaImage;
            console.debug('验证码加载成功:', captcha.id);
        })
        .catch((error) => {
            console.error('获取验证码失败:', error);
            message.error('获取验证码失败,请重试');
        })
        .finally(() => {
            captcha.loading = false;
        });
}

// 刷新验证码
function refreshCaptcha() {
    if (!config.validateCodeEnable) return;

    captcha.loading = true;
    form.value.captcha = ''; // 清空输入
    errors.captcha = '';     // 清空错误

    JBoltApi.tryGet('/captcha/refresh')
        .then((res) => {
            captcha.id = res.data.captchaId;
            captcha.image = res.data.captchaImage;
            console.debug('验证码刷新成功:', captcha.id);
        })
        .catch((error) => {
            console.error('刷新验证码失败:', error);
            message.error('刷新验证码失败');
        })
        .finally(() => {
            captcha.loading = false;
        });
}

// 获取系统配置
function loadConfig() {
    JBoltApi.tryGet('/admin/globalConfig/getByKeys?keys=jaOLT_LOGIN_USE_CAPTURE')
        .then((res) => {
            config.validateCodeEnable = res.data.jaOLT_LOGIN_USE_CAPTURE;
            
            // 如果开启了验证码,则加载验证码
            if (config.validateCodeEnable) {
                loadCaptcha();
            }
        })
        .catch((error) => {
            console.error('获取系统配置失败:', error);
            // 使用默认配置
            config.validateCodeEnable = true;
            loadCaptcha();
        });
}

// 登录处理
function handleLogin() {
    if (!validateForm()) return;

    loading.value = true;

    const loginData = {
        userName: form.value.username,
        password: processPassword(form.value.password),
    };

    // 如果开启了验证码,添加验证码信息
    if (config.validateCodeEnable) {
        loginData.captchaCode = form.value.captcha;
        loginData.captchaId = captcha.id;
    }

    JBoltApi.tryPost("/auth/login", loginData)
        .then((res) => {
            // 登录成功处理
            auth.login(res.data.user, res.data.token);
            router.push("/jboltai");
        })
        .catch((error) => {
            console.error('登录失败:', error);
            // 登录失败时刷新验证码
            if (config.validateCodeEnable) {
                refreshCaptcha();
            }
        })
        .finally(() => {
            loading.value = false;
        });
}

// 生命周期
onMounted(() => {
    loadConfig();
});

// 监听配置变化
watch(() => config.validateCodeEnable, (enabled) => {
    if (enabled && !captcha.image && !captcha.loading) {
        loadCaptcha();
    }
});
</script>

<template>
    <!-- 验证码组件使用 -->
    <captcha-component
        v-if="config.validateCodeEnable"
        v-model="form.captcha"
        :error="errors.captcha"
        :captcha-image="captcha.image"
        :loading="captcha.loading"
        :focus-field="focusField"
        :hover-field="hoverField"
        @refresh="refreshCaptcha"
        @focus="focusField = 'captcha'"
        @blur="focusField = ''"
        @mouseenter="hoverField = 'captcha'"
        @mouseleave="hoverField = ''"
    />
</template>

🚀 完整实现步骤

步骤1:创建后端缓存工具类

// src/main/java/cn/jbolt/util/cache/CaptchaCache.java
package cn.jbolt.util.cache;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

public class CaptchaCache {
    
    private static final Map<String, CaptchaInfo> CACHE = new ConcurrentHashMap<>();
    
    private static class CaptchaInfo {
        private final String code;
        private final long expireTime;
        
        public CaptchaInfo(String code, long expireTime) {
            this.code = code;
            this.expireTime = expireTime;
        }
        
        public boolean isExpired() {
            return System.currentTimeMillis() > expireTime;
        }
        
        public String getCode() {
            return code;
        }
    }
    
    /**
     * 存储验证码
     */
    public static boolean putCaptcha(String captchaId, String code, int timeout, TimeUnit timeUnit) {
        long expireTime = System.currentTimeMillis() + timeUnit.toMillis(timeout);
        CACHE.put(captchaId, new CaptchaInfo(code, expireTime));
        
        // 清理过期数据
        cleanExpiredCache();
        return true;
    }
    
    /**
     * 验证验证码
     */
    public static boolean verifyCaptcha(String captchaId, String inputCode, boolean removeAfterVerify) {
        CaptchaInfo info = CACHE.get(captchaId);
        
        if (info == null || info.isExpired()) {
            CACHE.remove(captchaId);
            return false;
        }
        
        boolean isValid = info.getCode().equalsIgnoreCase(inputCode.trim());
        
        if (removeAfterVerify) {
            CACHE.remove(captchaId);
        }
        
        return isValid;
    }
    
    /**
     * 清理过期缓存
     */
    private static void cleanExpiredCache() {
        CACHE.entrySet().removeIf(entry -> entry.getValue().isExpired());
    }
    
    /**
     * 获取缓存大小(用于监控)
     */
    public static int getCacheSize() {
        cleanExpiredCache();
        return CACHE.size();
    }
}

步骤2:创建服务接口

// src/main/java/cn/jbolt/admin/user/service/CaptchaService.java
package cn.jbolt.admin.user.service;

import cn.jbolt.util.Result;

public interface CaptchaService {
    
    /**
     * 生成验证码(默认参数)
     */
    Result generateCaptcha();
    
    /**
     * 生成验证码(自定义参数)
     */
    Result generateCaptcha(int width, int height, int codeCount, int lineCount);
    
    /**
     * 验证验证码
     */
    Result verifyCaptcha(String captchaId, String inputCode);
    
    /**
     * 刷新验证码
     */
    Result refreshCaptcha();
}

步骤3:实现服务类

// src/main/java/cn/jbolt/admin/user/service/impl/CaptchaServiceImpl.java
// [完整代码见上文服务层实现]

步骤4:创建控制器

// src/main/java/cn/jbolt/admin/user/controller/CaptchaController.java
// [完整代码见上文控制器层实现]

步骤5:创建前端验证码组件

<!-- src/components/verificationCode.vue -->
<!-- [完整代码见上文前端组件实现] -->

步骤6:在登录页面集成

<!-- src/views/login/index.vue -->
<!-- [完整代码见上文登录页面集成] -->

⚙️ 配置说明

1. 验证码参数配置

// 在 CaptchaServiceImpl.java 中修改默认配置
private static final int DEFAULT_WIDTH = 200;        // 图片宽度
private static final int DEFAULT_HEIGHT = 100;       // 图片高度  
private static final int DEFAULT_CODE_COUNT = 4;     // 验证码字符数
private static final int DEFAULT_LINE_COUNT = 20;    // 干扰线数量
private static final int DEFAULT_EXPIRE_MINUTES = 5; // 过期时间(分钟)

2. 系统配置开关

在数据库配置表中添加:

-- 验证码开关配置
INSERT INTO sys_config (config_key, config_value, description) 
VALUES ('jaOLT_LOGIN_USE_CAPTURE', 'true', '登录是否启用验证码');

-- 版权信息配置
INSERT INTO sys_config (config_key, config_value, description) 
VALUES ('SYSTEM_COPYRIGHT_COMPANY', 'JBoltAI技术有限公司', '版权信息');

3. 前端配置

// 在 index.vue 中配置开发模式调试
const isDev = ref(process.env.NODE_ENV === 'development');

// API接口配置
const API_BASE_URL = {
    development: 'http://localhost:8080',
    production: 'https://api.yourdomian.com'
}[process.env.NODE_ENV];

📚 API接口文档

1. 生成验证码

接口地址: GET /captcha/generate

请求参数:

响应示例:

{
    "code": 200,
    "msg": "验证码生成成功",
    "data": {
        "captchaId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "captchaImage": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYA..."
    }
}

2. 生成自定义验证码

接口地址: GET /captcha/generate/custom

请求参数:

参数名类型必填默认值说明
widthint200图片宽度
heightint100图片高度
codeCountint4验证码字符数
lineCountint20干扰线数量

请求示例:

GET /captcha/generate/custom?width=250&height=80&codeCount=5&lineCount=30

3. 验证验证码

接口地址: POST /captcha/verify

请求参数:

参数名类型必填说明
captchaIdString验证码ID
codeString用户输入的验证码

请求示例:

{
    "captchaId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "code": "ABCD"
}

响应示例:

{
    "code": 200,
    "msg": "验证码验证成功",
    "data": true
}

4. 刷新验证码

接口地址: GET /captcha/refresh

请求参数:

响应示例: 同生成验证码接口


❗ 常见问题解决

1. 验证码图片不显示

可能原因:

  • 后端接口未正确返回Base64数据
  • 前端图片src格式错误
  • 网络请求失败

解决方案:

// 1. 检查返回的Base64格式
console.log('验证码图片数据:', captcha.image);

// 2. 确保Base64格式正确
if (captcha.image && !captcha.image.startsWith('data:image/')) {
    captcha.image = 'data:image/png;base64,' + captcha.image;
}

// 3. 添加图片加载错误处理
const onImageError = () => {
    console.warn('验证码图片加载失败,正在重新获取...');
    refreshCaptcha();
};

2. 验证码验证失败

可能原因:

  • 验证码已过期
  • 大小写不匹配
  • 验证码ID与输入不对应

解决方案:

// 在验证时添加详细日志
@Override
public Result verifyCaptcha(String captchaId, String inputCode) {
    logger.info("验证码验证开始: captchaId={}, inputCode={}", captchaId, inputCode);
    
    // 添加更详细的验证逻辑
    CaptchaInfo info = CaptchaCache.getCaptcha(captchaId);
    if (info == null) {
        logger.warn("验证码不存在: captchaId={}", captchaId);
        return Result.error("验证码不存在或已过期");
    }
    
    if (info.isExpired()) {
        logger.warn("验证码已过期: captchaId={}", captchaId);
        CaptchaCache.removeCaptcha(captchaId);
        return Result.error("验证码已过期,请刷新");
    }
    
    // 忽略大小写比较
    boolean isValid = info.getCode().equalsIgnoreCase(inputCode.trim());
    logger.info("验证码验证结果: captchaId={}, isValid={}", captchaId, isValid);
    
    return isValid ? Result.success(true) : Result.error("验证码错误");
}

3. 内存缓存占用过多

解决方案:

// 添加定时清理任务
@Component
public class CaptchaCacheCleanTask {
    
    @Scheduled(fixedRate = 60000) // 每分钟执行一次
    public void cleanExpiredCache() {
        int beforeSize = CaptchaCache.getCacheSize();
        CaptchaCache.cleanExpiredCache();
        int afterSize = CaptchaCache.getCacheSize();
        
        if (beforeSize != afterSize) {
            logger.info("清理过期验证码缓存: {} -> {}", beforeSize, afterSize);
        }
    }
}

4. 前端组件状态同步问题

解决方案:

// 使用 watch 监听配置变化
watch(() => config.validateCodeEnable, (enabled) => {
    console.debug('验证码配置变化:', enabled);
    if (enabled && !captcha.image && !captcha.loading) {
        console.debug('配置启用后自动加载验证码');
        loadCaptcha();
    }
});

// 添加组件状态调试
watch(() => captcha, (newCaptcha) => {
    console.debug('验证码状态变化:', {
        hasImage: !!newCaptcha.image,
        captchaId: newCaptcha.id,
        loading: newCaptcha.loading
    });
}, { deep: true });

🔧 扩展建议

1. 升级为Redis缓存

@Service
public class RedisCaptchaService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String CAPTCHA_PREFIX = "captcha:";
    
    public boolean putCaptcha(String captchaId, String code, int timeout, TimeUnit timeUnit) {
        String key = CAPTCHA_PREFIX + captchaId;
        redisTemplate.opsForValue().set(key, code, timeout, timeUnit);
        return true;
    }
    
    public boolean verifyCaptcha(String captchaId, String inputCode, boolean removeAfterVerify) {
        String key = CAPTCHA_PREFIX + captchaId;
        String storedCode = redisTemplate.opsForValue().get(key);
        
        if (storedCode == null) {
            return false;
        }
        
        boolean isValid = storedCode.equalsIgnoreCase(inputCode.trim());
        
        if (removeAfterVerify) {
            redisTemplate.delete(key);
        }
        
        return isValid;
    }
}

2. 添加验证码类型扩展

public enum CaptchaType {
    LINE,      // 线性验证码
    CIRCLE,    // 圆圈验证码  
    SHEAR,     // 扭曲验证码
    GIF        // 动态验证码
}

@Service
public class EnhancedCaptchaService {
    
    public Result generateCaptcha(CaptchaType type, int width, int height, int codeCount) {
        AbstractCaptcha captcha;
        
        switch (type) {
            case LINE:
                captcha = CaptchaUtil.createLineCaptcha(width, height, codeCount, 20);
                break;
            case CIRCLE:
                captcha = CaptchaUtil.createCircleCaptcha(width, height, codeCount, 20);
                break;
            case SHEAR:
                captcha = CaptchaUtil.createShearCaptcha(width, height, codeCount, 4);
                break;
            case GIF:
                captcha = CaptchaUtil.createGifCaptcha(width, height, codeCount);
                break;
            default:
                captcha = CaptchaUtil.createLineCaptcha(width, height, codeCount, 20);
        }
        
        // 其他逻辑保持不变
        return generateCaptchaResult(captcha);
    }
}

3. 添加验证码统计功能

@Component
public class CaptchaMetrics {
    
    private final AtomicLong generateCount = new AtomicLong(0);
    private final AtomicLong verifyCount = new AtomicLong(0);
    private final AtomicLong successCount = new AtomicLong(0);
    
    public void recordGenerate() {
        generateCount.incrementAndGet();
    }
    
    public void recordVerify(boolean success) {
        verifyCount.incrementAndGet();
        if (success) {
            successCount.incrementAndGet();
        }
    }
    
    public Map<String, Object> getMetrics() {
        Map<String, Object> metrics = new HashMap<>();
        metrics.put("generateCount", generateCount.get());
        metrics.put("verifyCount", verifyCount.get());
        metrics.put("successCount", successCount.get());
        metrics.put("successRate", 
            verifyCount.get() > 0 ? (double) successCount.get() / verifyCount.get() : 0);
        return metrics;
    }
}

4. 前端增强功能

<script setup>
// 添加键盘事件支持
const handleKeydown = (event) => {
    if (event.key === 'Enter') {
        handleLogin();
    } else if (event.key === 'F5' || (event.ctrlKey && event.key === 'r')) {
        event.preventDefault();
        refreshCaptcha();
    }
};

// 添加验证码倒计时
const countdown = ref(0);
const startCountdown = () => {
    countdown.value = 300; // 5分钟倒计时
    const timer = setInterval(() => {
        countdown.value--;
        if (countdown.value <= 0) {
            clearInterval(timer);
            refreshCaptcha(); // 自动刷新
        }
    }, 1000);
};

// 添加无障碍支持
const speakCaptcha = () => {
    if ('speechSynthesis' in window) {
        const utterance = new SpeechSynthesisUtterance('请输入验证码');
        speechSynthesis.speak(utterance);
    }
};

onMounted(() => {
    window.addEventListener('keydown', handleKeydown);
});

onUnmounted(() => {
    window.removeEventListener('keydown', handleKeydown);
});
</script>

📝 总结

本技术文档详细介绍了基于Hutool工具类实现的完整验证码功能,包含:

  1. 完整的技术栈:Spring Boot + Hutool + Vue3 + Tailwind CSS
  2. 安全的设计:验证码ID与答案分离,过期自动清理
  3. 良好的用户体验:响应式设计,自动刷新,错误处理
  4. 可扩展性:支持Redis缓存,多种验证码类型,统计功能
  5. 详细的实现步骤:从零开始的完整实现指南

通过这个文档,即使是技术小白也能够:

  • 理解验证码功能的完整架构
  • 按步骤实现所有功能
  • 解决常见问题
  • 根据需要进行功能扩展

希望这个文档能够帮助您快速掌握验证码功能的开发!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yzhSWJ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值