Spring Boot 3 滑动验证码集成:Tianai-Captcha 完整实战指南 本文摘要
本文将带你从零搭建一个符合阿里巴巴规范的 Spring Boot 3 项目,并集成 Tianai-Captcha 验证码组件。重点讲解如何通过配置动态切换验证码类型(滑动/旋转/文字点选),以及生产级配置(Redis 缓存、资源加载、二次验证)。
本文设计到的前后端我已上传至文件存储服务,请有需要的朋友按需下载(Vue3 + SpringBoot3)
第一章. 快速起步:30 秒搭建项目骨架 在开始集成验证码之前, 我们需要先搭建一个标准的 Spring Boot 3 项目骨架。这一章会直接提供完整的基础代码, 你只需要复制粘贴即可。
1.1. 环境与依赖清单 环境要求 JDK 17+ Maven 3.8+ Spring Boot 3.5.9 IDEA 2023+(推荐) Maven 依赖配置
打开 pom.xml, 复制以下依赖块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 3.5.9</version > <relativePath /> </parent > <groupId > com.example</groupId > <artifactId > captcha</artifactId > <version > 1.0.0</version > <properties > <java.version > 17</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > cloud.tianai.captcha</groupId > <artifactId > tianai-captcha-springboot-starter</artifactId > <version > 1.5.3</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-core</artifactId > <version > 5.8.25</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency > </dependencies > </project >
1.2. 项目结构与基础代码 完整目录树
我们会完成一个 demo 来帮助您快速接入 Tianai-Captcha,后端项目目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 src/main/java/com/example/captcha ├── CaptchaApplication.java # 启动类 ├── controller │ └── CaptchaController.java # 验证码接口 ├── service │ ├── CaptchaService.java # 验证码服务接口 │ └── impl │ └── CaptchaServiceImpl.java # 验证码服务实现 ├── config │ ├── CaptchaProperties.java # 验证码配置类 │ └── CaptchaResourceConfig.java # 资源加载配置 ├── common │ ├── Result.java # 统一响应封装 │ ├── ResultCode.java # 响应码枚举 │ └── exception │ ├── BizException.java # 业务异常 │ └── GlobalExceptionHandler.java # 全局异常处理 └── model ├── dto │ ├── CaptchaCheckDTO.java # 验证码校验请求 │ └── CaptchaVO.java # 验证码响应 └── enums └── CaptchaTypeEnum.java # 验证码类型枚举 src/main/resources ├── application.yml # 配置文件 └── captcha ├── templates # 验证码模板 │ ├── slider │ │ ├── 1 │ │ │ ├── active.png │ │ │ └── fixed.png │ │ └── 2 │ │ ├── active.png │ │ └── fixed.png │ └── rotate │ └── 1 │ ├── active.png │ └── fixed.png └── backgrounds # 背景图片 ├── 1.jpg ├── 2.jpg └── 3.jpg
基础代码(直接复制)
步骤 1:统一响应封装
📄 文件:src/main/java/com/example/captcha/common/Result.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.example.captcha.common;import lombok.Data;import java.io.Serializable;@Data public class Result <T> implements Serializable { private Integer code; private String message; private T data; private Result (Integer code, String message, T data) { this .code = code; this .message = message; this .data = data; } public static <T> Result<T> success (T data) { return new Result <>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); } public static <T> Result<T> success () { return success(null ); } public static <T> Result<T> fail (String message) { return new Result <>(ResultCode.FAIL.getCode(), message, null ); } public static <T> Result<T> fail (Integer code, String message) { return new Result <>(code, message, null ); } }
步骤 2:响应码枚举
📄 文件:src/main/java/com/example/captcha/common/ResultCode.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.example.captcha.common;import lombok.Getter;@Getter public enum ResultCode { SUCCESS(200 , "操作成功" ), FAIL(500 , "操作失败" ), PARAM_ERROR(400 , "参数错误" ), CAPTCHA_EXPIRED(4001 , "验证码已过期" ), CAPTCHA_ERROR(4002 , "验证码错误" ); private final Integer code; private final String message; ResultCode(Integer code, String message) { this .code = code; this .message = message; } }
步骤 3:业务异常类
📄 文件:src/main/java/com/example/captcha/common/exception/BizException.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.example.captcha.common.exception;import lombok.Getter;@Getter public class BizException extends RuntimeException { private final Integer code; public BizException (String message) { super (message); this .code = 500 ; } public BizException (Integer code, String message) { super (message); this .code = code; } }
步骤 4:全局异常处理
📄 文件:src/main/java/com/example/captcha/common/exception/GlobalExceptionHandler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.example.captcha.common.exception;import com.example.captcha.common.Result;import lombok.extern.slf4j.Slf4j;import org.springframework.validation.BindException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BizException.class) public Result<?> handleBizException(BizException e) { log.error("业务异常: {}" , e.getMessage()); return Result.fail(e.getCode(), e.getMessage()); } @ExceptionHandler(BindException.class) public Result<?> handleBindException(BindException e) { String message = e.getBindingResult().getFieldError().getDefaultMessage(); log.error("参数校验失败: {}" , message); return Result.fail(400 , message); } @ExceptionHandler(Exception.class) public Result<?> handleException(Exception e) { log.error("系统异常" , e); return Result.fail("系统异常,请稍后重试" ); } }
至此, 项目骨架搭建完成。接下来我们开始集成 Tianai-Captcha。
第二章. Tianai-Captcha 核心概念与设计理念 在开始编码之前, 我们需要理解 Tianai-Captcha 的核心设计理念和关键组件。
2.1. 为什么选择 Tianai-Captcha? 在 Spring Boot 3 环境下, Tianai-Captcha 是目前最佳选择:
对比维度 Tianai-Captcha AJ-Captcha Spring Boot 3 支持 ✅ 原生支持 ❌ 需要手动适配 维护状态 ✅ 活跃维护 ⚠️ 更新较慢 包名兼容 ✅ 使用 jakarta.servlet ❌ 使用 javax.servlet UI 现代化 ✅ 现代化设计 ⚠️ 较为传统 集成难度 ✅ 开箱即用 ❌ 需要额外配置
2.2. 四种验证码类型对比 Tianai-Captcha 支持四种验证码类型, 每种类型适用于不同的业务场景:
类型 常量 用户操作 适用场景 安全等级 滑动验证码 SLIDER拖动滑块到缺口位置 登录、注册、评论 ⭐⭐⭐ 旋转验证码 ROTATE旋转图片到正确角度 支付、敏感操作 ⭐⭐⭐⭐ 滑动还原验证码 CONCAT拖动图片碎片拼接完整 高安全场景 ⭐⭐⭐⭐ 文字点选验证码 WORD_IMAGE_CLICK按顺序点击指定文字 防机器人、高安全 ⭐⭐⭐⭐⭐
选择建议 :
普通场景(登录/注册):使用 SLIDER,用户体验最好 支付场景:使用 ROTATE 或 CONCAT,安全性更高 防刷场景:使用 WORD_IMAGE_CLICK,机器人难以破解 2.3. 核心 API 速查 ImageCaptchaApplication(核心门面类) 这是 Tianai-Captcha 提供的核心服务类, 所有验证码操作都通过它完成:
方法 参数 返回值 说明 generateCaptcha(String type)type:验证码类型 ApiResponse<ImageCaptchaVO>生成验证码 matching(String id, ImageCaptchaTrack track)id:验证码 ID track:滑动轨迹 ApiResponse<?>校验验证码
ImageCaptchaVO(验证码数据对象) 生成验证码后返回的核心数据结构:
字段名 类型 说明 注意事项 idString 验证码唯一 ID 用于后续校验 typeString 验证码类型 SLIDER/ROTATE/CONCAT/WORD_IMAGE_CLICK backgroundImageString 背景图(Base64) 格式:data: image/png; base64,… templateImageString 滑块图(Base64) backgroundImageWidthInteger 背景图宽度(像素) 前端需要传回 backgroundImageHeightInteger 背景图高度(像素) 前端需要传回 templateImageWidthInteger 滑块图宽度(像素) templateImageHeightInteger 滑块图高度(像素)
ImageCaptchaTrack(滑动轨迹对象) 我们以三方的社区 vue3 最新社区插件(kite-captcha-vue) 为例:前端传回的用户滑动轨迹数据:
前端字段 说明 bgImageWidth背景图宽度 bgImageHeight背景图高度 sliderImageWidth滑块图宽度 sliderImageHeight滑块图高度 startSlidingTime开始时间 endSlidingTime结束时间 trackList[].t轨迹时间戳 trackList[].xX 坐标 trackList[].yY 坐标 trackList[].type轨迹类型
轨迹数据的作用 :
Tianai-Captcha 会通过分析轨迹数据来判断是否为真人操作:
分析维度 检测内容 说明 位置准确性 最终 X 坐标是否在缺口位置 允许 ±5 像素误差 滑动速度 总时长是否合理 太快(<100ms)或太慢(> 10s)都可疑 轨迹平滑度 坐标点之间的间隔是否均匀 机器人通常是匀速直线 加速度变化 速度是否有加速和减速 人类滑动会有加速和减速过程
第三章. 多态设计:通过配置动态切换验证码类型 在实际业务中, 我们可能需要根据不同场景使用不同类型的验证码。硬编码验证码类型会导致代码难以维护, 因此我们需要通过配置来动态切换。
3.1. 设计思路:策略模式 + 配置驱动 为什么不能硬编码验证码类型?
假设我们在代码中写死了 SLIDER 类型:
1 2 3 4 5 6 public CaptchaVO generateCaptcha () { ApiResponse<ImageCaptchaVO> response = imageCaptchaApplication.generateCaptcha(CaptchaTypeConstant.SLIDER); }
这样做的问题:
如果需要切换到 ROTATE 类型, 必须修改代码并重新部署 无法根据不同业务场景使用不同类型 测试环境和生产环境无法使用不同配置 正确做法:配置驱动
我们通过 application.yml 配置默认类型, 同时支持前端传参动态指定:
1 2 captcha: default-type: SLIDER
1 2 3 4 5 6 7 8 public CaptchaVO generateCaptcha (String type) { String captchaType = type != null ? type : captchaProperties.getDefaultType(); ApiResponse<ImageCaptchaVO> response = imageCaptchaApplication.generateCaptcha(captchaType); }
3.2. 验证码类型枚举 首先定义验证码类型枚举, 用于类型校验和转换:
📄 文件:src/main/java/com/example/captcha/model/enums/CaptchaTypeEnum.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.example.captcha.model.enums;import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;import lombok.Getter;@Getter public enum CaptchaTypeEnum { SLIDER("SLIDER" , "滑动验证码" , CaptchaTypeConstant.SLIDER), ROTATE("ROTATE" , "旋转验证码" , CaptchaTypeConstant.ROTATE), CONCAT("CONCAT" , "滑动还原验证码" , CaptchaTypeConstant.CONCAT), WORD_IMAGE_CLICK("WORD_IMAGE_CLICK" , "文字点选验证码" , CaptchaTypeConstant.WORD_IMAGE_CLICK); private final String code; private final String desc; private final String constant; CaptchaTypeEnum(String code, String desc, String constant) { this .code = code; this .desc = desc; this .constant = constant; } public static CaptchaTypeEnum getByCode (String code) { for (CaptchaTypeEnum typeEnum : values()) { if (typeEnum.getCode().equals(code)) { return typeEnum; } } return null ; } public static boolean isValid (String code) { return getByCode(code) != null ; } }
3.3. 配置类实现 📄 文件:src/main/java/com/example/captcha/config/CaptchaProperties.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package com.example.captcha.config;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;import java.util.HashMap;import java.util.List;import java.util.Map;@Data @Component @ConfigurationProperties(prefix = "captcha") public class CaptchaProperties { private String defaultType = "SLIDER" ; private List<String> enabledTypes = List.of("SLIDER" , "ROTATE" , "WORD_IMAGE_CLICK" ); private Map<String, Long> expire = new HashMap <>() {{ put("SLIDER" , 120000L ); put("ROTATE" , 120000L ); put("CONCAT" , 120000L ); put("WORD_IMAGE_CLICK" , 180000L ); }}; private boolean secondaryEnabled = false ; private Long secondaryExpire = 120000L ; public boolean isTypeEnabled (String type) { return enabledTypes.contains(type); } public Long getExpireTime (String type) { return expire.getOrDefault(type, 120000L ); } }
3.4. 配置文件设计 📄 文件:src/main/resources/application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 server: port: 8080 spring: application: name: captcha-demo captcha: default-type: SLIDER enabled-types: - SLIDER - ROTATE - WORD_IMAGE_CLICK expire: SLIDER: 120000 ROTATE: 120000 CONCAT: 120000 WORD_IMAGE_CLICK: 180000 secondary-enabled: false secondary-expire: 120000 init-default-resource: true local-cache-enabled: true local-cache-size: 20 local-cache-wait-time: 5000 local-cache-period: 2000 prefix: "captcha"
3.5. 请求对象定义 由于 Tianai-Captcha 已经提供了完整的响应对象 ImageCaptchaVO 和轨迹对象 ImageCaptchaTrack,我们只需要定义一个校验请求的包装类:
根据前端三方社区 kite-captcha-vue 的文档和 Tianai-Captcha 后端的 ImageCaptchaTrack 源码。
前端 CaptchaMatchingData 结构:
1 2 3 4 5 6 7 8 9 10 interface MatchingData { bgImageWidth : number ; bgImageHeight : number ; sliderImageWidth : number ; sliderImageHeight : number ; startSlidingTime : string ; endSlidingTime : string ; trackList : CaptchaTrack []; data ?: any ; }
后端 ImageCaptchaTrack 结构:
1 2 3 4 5 6 7 8 9 10 11 12 public class ImageCaptchaTrack { private Integer bgImageWidth; private Integer bgImageHeight; private Integer templateImageWidth; private Integer templateImageHeight; private Long startTime; private Long stopTime; private Integer left; private Integer top; private List<Track> trackList; private Object data; }
存在差异:
前端字段 后端字段 差异说明 sliderImageWidthtemplateImageWidth字段名不同 sliderImageHeighttemplateImageHeight字段名不同 startSlidingTime (String)startTime (Long)字段名不同,类型也不同 endSlidingTime (String)stopTime (Long)字段名不同,类型也不同
所以前后端数据结构 并不是完全一致的 ,需要做字段映射和类型转换。
📄 文件:src/main/java/com/example/captcha/model/dto/CaptchaCheckDTO.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 package com.example.captcha.model.dto;import cn.hutool.core.bean.BeanUtil;import cn.hutool.core.bean.copier.CopyOptions;import cn.hutool.core.date.LocalDateTimeUtil;import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;import com.fasterxml.jackson.annotation.JsonProperty;import jakarta.validation.constraints.NotBlank;import jakarta.validation.constraints.NotNull;import lombok.Data;import java.util.List;@Data public class CaptchaCheckDTO { @NotBlank(message = "验证码ID不能为空") private String id; @NotNull(message = "轨迹数据不能为空") @JsonProperty("data") private TrackData data; @Data public static class TrackData { private Integer bgImageWidth; private Integer bgImageHeight; private Integer sliderImageWidth; private Integer sliderImageHeight; private String startSlidingTime; private String endSlidingTime; private List<Track> trackList; private Object data; } @Data public static class Track { private Float x; private Float y; private Float t; private String type; } public ImageCaptchaTrack toImageCaptchaTrack () { CopyOptions options = CopyOptions.create() .setFieldMapping(java.util.Map.of( "sliderImageWidth" , "templateImageWidth" , "sliderImageHeight" , "templateImageHeight" )); ImageCaptchaTrack track = BeanUtil.toBean(data, ImageCaptchaTrack.class, options); track.setStartTime(parseTimeToMills(data.getStartSlidingTime())); track.setStopTime(parseTimeToMills(data.getEndSlidingTime())); if (data.getTrackList() != null ) { track.setTrackList(BeanUtil.copyToList(data.getTrackList(), ImageCaptchaTrack.Track.class)); } return track; } private Long parseTimeToMills (String timeStr) { if (timeStr == null ) return null ; return LocalDateTimeUtil.parse(timeStr, "yyyy-MM-dd HH:mm:ss.SSS" ) .atZone(java.time.ZoneId.systemDefault()) .toInstant() .toEpochMilli(); } }
Service 层调用调整
在 CaptchaServiceImpl.java 中我们就能这样使用:
1 2 3 4 5 6 7 8 9 10 @Override public Map<String, Object> checkCaptcha (CaptchaCheckDTO checkDTO) { ImageCaptchaTrack track = checkDTO.toImageCaptchaTrack(); ApiResponse<?> response = imageCaptchaApplication.matching(checkDTO.getId(), track); }
为什么不需要定义 CaptchaVO?
官方的 ImageCaptchaVO 已经包含了所有必要的字段(id、type、backgroundImage、templateImage 等),直接使用即可,无需再创建一个业务 VO 去复制这些字段。
3.6. Service 层实现 Service 接口
📄 文件:src/main/java/com/example/captcha/service/CaptchaService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.example.captcha.service;import cloud.tianai.captcha.application.vo.ImageCaptchaVO;import com.example.captcha.model.dto.CaptchaCheckDTO;public interface CaptchaService { ImageCaptchaVO generateCaptcha (String type) ; boolean checkCaptcha (CaptchaCheckDTO checkDTO) ; }
Service 实现类
📄 文件:src/main/java/com/example/captcha/service/impl/CaptchaServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 package com.example.captcha.service.impl;import cloud.tianai.captcha.application.ImageCaptchaApplication;import cloud.tianai.captcha.application.vo.ImageCaptchaVO;import cloud.tianai.captcha.common.response.ApiResponse;import com.example.captcha.common.ResultCode;import com.example.captcha.common.exception.BizException;import com.example.captcha.config.CaptchaProperties;import com.example.captcha.model.dto.CaptchaCheckDTO;import com.example.captcha.model.enums.CaptchaTypeEnum;import com.example.captcha.service.CaptchaService;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;@Slf4j @Service @RequiredArgsConstructor public class CaptchaServiceImpl implements CaptchaService { private final ImageCaptchaApplication imageCaptchaApplication; private final CaptchaProperties captchaProperties; @Override public ImageCaptchaVO generateCaptcha (String type) { String captchaType = determineCaptchaType(type); ApiResponse<ImageCaptchaVO> response = imageCaptchaApplication.generateCaptcha(captchaType); if (!response.isSuccess()) { log.error("生成验证码失败: {}" , response.getMsg()); throw new BizException ("生成验证码失败" ); } ImageCaptchaVO captchaVO = response.getData(); log.info("生成验证码成功, id: {}, type: {}" , captchaVO.getId(), captchaVO.getType()); return captchaVO; } @Override public Map<String, Object> checkCaptcha (CaptchaCheckDTO checkDTO) { ImageCaptchaTrack track = checkDTO.toImageCaptchaTrack(); ApiResponse<?> response = imageCaptchaApplication.matching(checkDTO.getId(), track); if (!response.isSuccess()) { log.warn("验证码校验失败, id: {}, msg: {}" , checkDTO.getId(), response.getMsg()); throw new BizException (ResultCode.CAPTCHA_ERROR.getCode(), "验证码校验失败" ); } log.info("验证码校验成功, id: {}" , checkDTO.getId()); Map<String, Object> result = new HashMap <>(); result.put("success" , true ); result.put("validToken" , checkDTO.getId()); return result; } private String determineCaptchaType (String type) { if (type == null || type.isEmpty()) { return captchaProperties.getDefaultType(); } if (!CaptchaTypeEnum.isValid(type)) { log.warn("无效的验证码类型: {}, 使用默认类型: {}" , type, captchaProperties.getDefaultType()); throw new BizException (ResultCode.PARAM_ERROR.getCode(), "无效的验证码类型" ); } if (!captchaProperties.isTypeEnabled(type)) { log.warn("验证码类型未启用: {}, 使用默认类型: {}" , type, captchaProperties.getDefaultType()); throw new BizException (ResultCode.PARAM_ERROR.getCode(), "该验证码类型未启用" ); } return type; } }
3.7. Controller 层实现 📄 文件:src/main/java/com/example/captcha/controller/CaptchaController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 package com.example.captcha.controller;import cloud.tianai.captcha.application.vo.ImageCaptchaVO;import com.example.captcha.common.Result;import com.example.captcha.model.dto.CaptchaCheckDTO;import com.example.captcha.service.CaptchaService;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;import java.util.Map;@Slf4j @RestController @RequestMapping("/captcha") @RequiredArgsConstructor public class CaptchaController { private final CaptchaService captchaService; @PostMapping("/gen") public Result<ImageCaptchaVO> generateCaptcha ( @RequestParam(required = false) String type, @RequestBody(required = false) Map<String, Object> body) { log.info("生成验证码请求, type: {}" , type); if (type == null && body != null && body.containsKey("type" )) { type = (String) body.get("type" ); } ImageCaptchaVO imageCaptchaVO = captchaService.generateCaptcha(type); return Result.success(imageCaptchaVO); } @PostMapping("/check") public Result<Map<String, Object>> checkCaptcha (@Validated @RequestBody CaptchaCheckDTO checkDTO) { log.info("校验验证码请求, id: {}" , checkDTO.getId()); Map<String, Object> result = captchaService.checkCaptcha(checkDTO); return Result.success(result); } }
第四章. 生产级配置:资源加载与性能优化 在生产环境中,我们需要考虑性能优化、资源管理、分布式缓存等问题。本章将讲解如何进行生产级配置。
4.1. 自定义背景图与模板 官方图片规范
资源类型 尺寸要求 格式要求 说明 背景图 600×360 JPG 用于展示验证码的背景 滑块模板(active.png) 110×110 PNG(透明) 滑块图片 滑块模板(fixed.png) 110×110 PNG(透明) 凹槽图片 旋转模板(active.png) 200×200 PNG(透明) 旋转图片 旋转模板(fixed.png) 200×200 PNG(透明) 旋转凹槽
三种资源加载方式对比
加载方式 适用场景 优点 缺点 classpath资源打包在 JAR 内 部署简单,无需外部依赖 更新资源需要重新打包 file资源存储在服务器本地 可动态更新资源 需要维护文件路径 url资源存储在 OSS/CDN 支持分布式,易于管理 依赖网络,有延迟
资源配置实现
📄 文件:src/main/java/com/example/captcha/config/CaptchaResourceConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 package com.example.captcha.config;import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;import cloud.tianai.captcha.resource.CrudResourceStore;import cloud.tianai.captcha.resource.ResourceStore;import cloud.tianai.captcha.resource.common.model.dto.Resource;import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import jakarta.annotation.PostConstruct;@Slf4j @Component @RequiredArgsConstructor public class CaptchaResourceConfig { private final ResourceStore resourceStore; @PostConstruct public void init () { log.info("开始加载验证码资源..." ); CrudResourceStore crudResourceStore = (CrudResourceStore) resourceStore; addSliderTemplates(crudResourceStore); addRotateTemplates(crudResourceStore); addBackgroundImages(crudResourceStore); log.info("验证码资源加载完成" ); } private void addSliderTemplates (CrudResourceStore crudResourceStore) { ResourceMap template1 = new ResourceMap ("default" , 4 ); template1.put("active.png" , new Resource (ClassPathResourceProvider.NAME, "captcha/templates/slider_1/active.png" )); template1.put("fixed.png" , new Resource (ClassPathResourceProvider.NAME, "captcha/templates/slider_1/fixed.png" )); ResourceMap template2 = new ResourceMap ("default" , 4 ); template2.put("active.png" , new Resource (ClassPathResourceProvider.NAME, "captcha/templates/slider_2/active.png" )); template2.put("fixed.png" , new Resource (ClassPathResourceProvider.NAME, "captcha/templates/slider_2/fixed.png" )); crudResourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template1); crudResourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template2); log.info("滑动验证码模板加载完成,共 2 个模板" ); } private void addRotateTemplates (CrudResourceStore crudResourceStore) { ResourceMap template1 = new ResourceMap ("default" , 4 ); template1.put("active.png" , new Resource (ClassPathResourceProvider.NAME, "captcha/templates/rotate_1/active.png" )); template1.put("fixed.png" , new Resource (ClassPathResourceProvider.NAME, "captcha/templates/rotate_1/fixed.png" )); crudResourceStore.addTemplate(CaptchaTypeConstant.ROTATE, template1); log.info("旋转验证码模板加载完成,共 1 个模板" ); } private void addBackgroundImages (CrudResourceStore crudResourceStore) { crudResourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource ("classpath" , "captcha/backgrounds/1.png" , "default" )); crudResourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource ("classpath" , "captcha/backgrounds/2.png" , "default" )); crudResourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource ("classpath" , "captcha/backgrounds/3.png" , "default" )); crudResourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource ("classpath" , "captcha/backgrounds/1.png" , "default" )); crudResourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource ("classpath" , "captcha/backgrounds/2.png" , "default" )); log.info("背景图片加载完成,共 5 张图片" ); } }
注意事项 :
如果使用 classpath 方式,资源文件必须放在 src/main/resources 目录下 如果使用 file 方式,确保服务器有读取文件的权限 如果使用 url 方式,确保网络连接稳定,建议使用 CDN 加速 关于资源可以自取,将以下的压缩包下载后将 silider_resources 改为 resources 粘贴至 Java 资源文件夹即可
https://prorise-blog.oss-cn-guangzhou.aliyuncs.com/博客文件存储/silider_resources.rar
4.2. Redis 缓存集成 为什么需要 Redis?
在分布式环境下,验证码数据需要在多个服务实例之间共享。如果使用本地内存缓存,会导致以下问题:
验证码无法跨实例校验 :用户在实例 A 生成验证码,请求被负载均衡到实例 B 进行校验,会提示验证码不存在无法实现二次验证 :二次验证需要在校验成功后生成 token,如果使用本地缓存,token 无法跨实例共享幸运的是 Tianai-Captcha 已经帮我们做好了底层依赖,他会自动检测我们是否引入了 Redis 来自动执行缓存,默认键为:captcha: types
Redis 配置
在 pom.xml 引入 Redis 依赖,然后在 application.yml 中配置 Redis 连接信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: data: redis: host: localhost port: 6379 password: database: 0 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1ms
验证 Redis 是否生效
启动项目后,生成一个验证码,然后在 Redis 中查看是否有对应的 key:
1 2 3 4 5 6 7 8 redis-cli keys captcha:* get captcha:slider:xxxxxx
如果能看到 key,说明 Redis 集成成功。
4.3. 本地缓存优化 为什么需要本地缓存?
实时生成验证码需要进行图片处理(裁剪、合成、Base64 编码),在高并发场景下可能会成为性能瓶颈。Tianai-Captcha 提供了本地缓存机制,可以提前生成一批验证码缓存起来,用户请求时直接返回缓存的验证码。
缓存配置
在 application.yml 中开启本地缓存:
1 2 3 4 5 6 7 8 9 captcha: local-cache-enabled: true local-cache-size: 20 local-cache-wait-time: 5000 local-cache-period: 2000
缓存参数说明
参数 说明 推荐值 local-cache-enabled是否开启本地缓存 true(生产环境推荐开启) local-cache-size缓存大小 20-50(根据并发量调整) local-cache-wait-time缓存拉取失败后等待时间 5000ms local-cache-period缓存检查间隔 2000ms
性能对比
场景 未开启缓存 开启缓存 性能提升 单次请求耗时 50-100ms 5-10ms 10 倍 并发 100 QPS CPU 占用 30% CPU 占用 5% 6 倍
4.4. 二次验证机制 什么是二次验证?
二次验证是指在验证码校验成功后,生成一个临时 token 返回给前端,前端在后续的业务请求中携带这个 token,后端通过校验 token 来确认用户已经通过了验证码验证。
为什么需要二次验证?
假设我们的登录流程是这样的:
前端调用 /captcha/gen 生成验证码 用户完成验证码,前端调用 /captcha/check 校验验证码 校验成功后,前端调用 /user/login 进行登录 在这个流程中,如果没有二次验证,攻击者可以:
自己完成验证码验证 拿到验证码 ID 使用这个 ID 进行暴力破解登录接口 有了二次验证后,流程变为:
前端调用 /captcha/gen 生成验证码 用户完成验证码,前端调用 /captcha/check 校验验证码 校验成功后,后端返回一个临时 token 前端调用 /user/login 时携带这个 token 后端校验 token 是否有效,有效则允许登录 开启二次验证
在 application.yml 中开启二次验证:
1 2 3 4 5 captcha: secondary-enabled: true secondary-expire: 120000
Service 层实现二次验证
修改 CaptchaServiceImpl.java,在校验成功后生成 token:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Override public Map<String, Object> checkCaptcha (CaptchaCheckDTO checkDTO) { ApiResponse<?> response = imageCaptchaApplication.matching(checkDTO.getId(), checkDTO.getTrack()); if (!response.isSuccess()) { log.warn("验证码校验失败, id: {}, msg: {}" , checkDTO.getId(), response.getMsg()); throw new BizException (ResultCode.CAPTCHA_ERROR.getCode(), "验证码校验失败" ); } log.info("验证码校验成功, id: {}" , checkDTO.getId()); Map<String, Object> result = new HashMap <>(); result.put("success" , true ); result.put("validToken" , checkDTO.getId()); return result; } public boolean secondaryVerification (String validToken) { if (!captchaProperties.isSecondaryEnabled()) { throw new BizException ("二次验证未开启" ); } boolean valid = ((SecondaryVerificationApplication) imageCaptchaApplication) .secondaryVerification(validToken); if (!valid) { log.warn("二次验证失败, validToken: {}" , validToken); throw new BizException (ResultCode.CAPTCHA_ERROR.getCode(), "验证码已失效,请重新验证" ); } log.info("二次验证成功, validToken: {}" , validToken); return true ; }
修改 Service 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Map<String, Object> checkCaptcha (CaptchaCheckDTO checkDTO) ; boolean secondaryVerification (String validToken) ;
修改 Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @PostMapping("/login") public Result<?> login(@RequestParam String validToken, @RequestParam String username, @RequestParam String password) { captchaService.secondaryVerification(validToken); log.info("用户 {} 登录成功" , username); return Result.success("登录成功" ); }
二次验证的完整流程:
1 2 3 4 5 6 7 8 9 10 11 1. 前端调用 /captcha/gen 生成验证码 ↓ 2. 用户完成验证码,前端调用 /captcha/check 校验 ↓ 3. 后端返回 { "success": true, "validToken": "xxx" } ↓ 4. 前端调用 /login?validToken=xxx&username=xxx&password=xxx ↓ 5. 后端调用 secondaryVerification(validToken) 进行二次验证 ↓ 6. 验证通过后执行登录逻辑
第五章. 前端集成:Vue 3 + kite-captcha-vue 完整对接 本章摘要
后端接口已经就绪,但验证码终究要在用户界面上呈现。本章将带你从零搭建 Vue 3 前端项目,集成社区维护的 kite-captcha-vue 组件,并完成与后端的完整联调——包括验证码生成、校验、以及二次验证的登录流程演示。
本章学习路径
阶段 内容 认知里程碑 阶段一 后端跨域配置 理解 CORS 原理,解决前后端分离的跨域问题 阶段二 前端项目初始化 掌握 Vite + Vue 3 + TypeScript 项目搭建 阶段三 组件集成与 API 封装 完成验证码组件接入,实现类型安全的请求层 阶段四 登录流程联调 串联完整业务流程,验证二次验证机制
5.1. 后端跨域配置 在上一章中,我们完成了后端所有接口的开发。但当前端尝试调用这些接口时,浏览器会因为 “同源策略” 而拦截请求。我们需要在后端配置 CORS(Cross-Origin Resource Sharing,跨域资源共享)来解决这个问题。
5.1.1. 同源策略与 CORS 浏览器的同源策略规定:只有当协议、域名、端口三者完全相同时,才允许 JavaScript 发起请求。我们的场景中:
端 地址 说明 前端 http://localhost:5173Vite 默认开发端口 后端 http://localhost:8080Spring Boot 默认端口
端口不同,属于跨域请求。后端需要显式声明 “允许来自 5173 端口的请求”,浏览器才会放行。
5.1.2. CORS 配置类实现 📄 文件:src/main/java/com/example/captcha/config/CorsConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.example.captcha.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.cors.CorsConfiguration;import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import org.springframework.web.filter.CorsFilter;@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter () { CorsConfiguration config = new CorsConfiguration (); config.addAllowedOrigin("http://localhost:5173" ); config.setAllowCredentials(true ); config.addAllowedMethod("*" ); config.addAllowedHeader("*" ); config.setMaxAge(3600L ); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (); source.registerCorsConfiguration("/**" , config); return new CorsFilter (source); } }
配置完成后,重启后端服务即可生效。
5.2. 前端项目初始化 Tianai-Captcha 官方提供的前端 SDK 更新较为缓慢,已经无法满足最新版本的对接需求。社区开发者 stary1993 基于 Vue 3 + TypeScript 封装了 kite-captcha-vue 组件,提供了更现代化的集成方案。
kite-captcha-vue 核心特性 :
特性 说明 多种验证类型 支持滑动、旋转、拼图、文字点选四种验证方式 TypeScript 支持 完整的类型定义,开发体验友好 国际化 内置中/英/日/韩多语言支持 主题切换 支持亮色/暗黑主题 响应式设计 支持移动端触摸操作,可配置缩放比例
5.2.1. 创建 Vite 项目 打开终端,执行以下命令创建项目:
1 2 3 4 5 6 7 8 pnpm create vite captcha-frontend --template vue-ts cd captcha-frontendpnpm install
执行完成后,可以先启动项目确认环境正常:
浏览器访问 http://localhost:5173,看到 Vite + Vue 的欢迎页面即表示项目创建成功。
5.2.2. 安装核心依赖 我们需要安装以下依赖:
1 2 3 4 5 6 7 pnpm add kite-captcha-vue pnpm add vue-i18n pnpm add axios
5.2.3. 项目目录结构 安装完成后,我们按照以下结构组织代码:
1 2 3 4 5 6 7 8 9 10 11 12 src ├── api │ ├── index.ts # Axios 实例配置 │ ├── captcha.ts # 验证码相关 API │ └── types.ts # 类型定义 ├── components │ └── LoginForm.vue # 登录表单组件 ├── views │ └── Login.vue # 登录页面 ├── App.vue # 根组件 ├── main.ts # 入口文件 └── style.css # 全局样式
5.3. API 请求层封装 在对接验证码组件之前,我们先封装好 API 请求层。这样做的好处是:统一处理响应解包、错误提示、类型约束。
5.3.1. 类型定义 首先定义后端接口的响应类型,确保类型安全:
📄 文件:src/api/types.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 export interface Result <T> { code : number message : string data : T } export interface CaptchaData { id : string type : string backgroundImage : string templateImage : string backgroundImageWidth : number backgroundImageHeight : number templateImageWidth : number templateImageHeight : number data ?: Record <string , unknown > } export interface CaptchaCheckResult { success : boolean validToken : string } export interface LoginParams { username : string password : string validToken : string }
5.3.2. Axios 实例配置 创建 Axios 实例,配置基础地址和响应拦截器:
📄 文件:src/api/index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import axios from 'axios' import type { Result } from './types' const request = axios.create ({ baseURL : 'http://localhost:8080' , timeout : 10000 }) request.interceptors .response .use ( (response ) => { const result = response.data as Result <unknown > if (result.code === 200 ) { return result.data as any } return Promise .reject (new Error (result.message || '请求失败' )) }, (error ) => { const message = error.response ?.data ?.message || error.message || '网络异常' return Promise .reject (new Error (message)) } ) export default request
5.3.3. 验证码 API 封装 封装验证码相关的 API 调用:
📄 文件:src/api/captcha.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import request from './index' import type { CaptchaData , CaptchaCheckResult } from './types' export function generateCaptcha (type ?: string ): Promise <CaptchaData > { return request.post ('/captcha/gen' , { type }) } export function checkCaptcha (data : { id: string ; data: unknown } ): Promise <CaptchaCheckResult > { return request.post ('/captcha/check' , data) } export function login (params : { username: string password: string validToken: string } ): Promise <string > { return request.post ('/captcha/login' , null , { params }) }
5.4. 验证码组件集成 API 层准备就绪,现在开始集成 kite-captcha-vue 组件。
5.4.1. 全局注册组件 在入口文件中注册验证码组件:
📄 文件:src/main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { createApp } from 'vue' import App from './App.vue' import { KiteCaptcha , KiteConfigProvider , i18n } from 'kite-captcha-vue' import 'kite-captcha-vue/dist/index.css' import './style.css' const app = createApp (App )app.component ('KiteCaptcha' , KiteCaptcha ) app.component ('KiteConfigProvider' , KiteConfigProvider ) app.use (i18n) app.mount ('#app' )
5.4.2. 登录表单组件 将验证码和表单逻辑封装为独立组件,便于复用和维护:
KiteCaptcha 组件提供了一下的核心内容,根据文档内容进行对接即可:
属性
属性名 类型 默认值 描述 v-model number or string- v-model 绑定值,用于接收验证结果状态码 scale number1 缩放比例 loadingAnimation string‘slide’ 加载动画类型,可选值:'slide' 'bounce' 'fade' 'lightSpeed' 'zoom' showClose booleantrue 是否显示关闭按钮 backgroundImage string- 背景图片 renderDataApi Function- 获取验证码数据的异步函数 matchingResultCodeProps Map验证结果状态码配置 expired 表示已过期 failed 表示验证失败 success 表示验证成功
对外暴露的方法
事件
事件名 参数 描述 render-success boolean验证码渲染成功回调 matching-data CaptchaMatchingData返回验证轨迹数据
📄 文件:src/components/LoginForm.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 <template> <div class="login-box"> <h3>登录 Demo</h3> <input v-model="form.user" placeholder="用户名" /> <input v-model="form.pass" type="password" placeholder="密码" /> <button @click="handleLogin">点击登录</button> <div v-if="showModal" class="mask" @click.self="showModal = false"> <div class="modal-body"> <KiteConfigProvider locale="zh_cn" theme="light"> <KiteCaptcha ref="captchaRef" v-model="resultCode" :render-data-api="fetchCaptcha" :matching-result-code-props="{ success: 200, failed: 4001, expired: 4000 }" :show-close="false" @matching-data="onMatch" /> </KiteConfigProvider> </div> </div> </div> </template> <script setup lang="ts"> import { generateCaptcha, checkCaptcha } from "../api/captcha"; import { ref, reactive } from "vue"; const form = reactive({ user: "", pass: "" }); const showModal = ref(false); // 控制弹窗 const captchaRef = ref(); const resultCode = ref<number | null>(null); // 控制验证码组件状态(绿钩/红叉) // 1. 点击登录 -> 打开弹窗 const handleLogin = () => { if (!form.user || !form.pass) return alert("请输入账号密码"); showModal.value = true; resultCode.value = null; // 重置状态 }; // 2. 获取验证码数据 (透传给组件) const fetchCaptcha = async () => await generateCaptcha("SLIDER"); // 3. 用户滑动结束 -> 校验 const onMatch = async (data: any) => { try { const res = await checkCaptcha(data); if (res.success) { resultCode.value = 200; // 让组件显示“成功”绿钩 // 延迟 0.5秒 关闭弹窗并登录 setTimeout(() => { showModal.value = false; alert(`校验通过,Token: ${res.validToken}\n即将提交登录...`); // TODO: 这里写真实的 login API 调用 }, 500); } } catch (e) { resultCode.value = 4002; // 让组件显示“失败”红叉 // 1秒后自动刷新 setTimeout(() => captchaRef.value?.refresh(), 1000); } }; </script> <style scoped> /* 最基础布局 */ .login-box { display: flex; flex-direction: column; width: 300px; margin: 50px auto; gap: 15px; } input, button { padding: 8px; } /* 弹窗蒙层 */ .mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; /* 垂直居中 */ justify-content: center; /* 水平居中 */ z-index: 999; } .modal-body { background: transparent; padding: 20px; border-radius: 8px; } </style>
5.4.3. 登录页面实现 登录页面负责整体布局,引入表单组件:
📄 文件:src/views/Login.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <template> <div class="login-container"> <div class="login-card"> <h2 class="login-title">用户登录</h2> <LoginForm /> </div> </div> </template> <script setup lang="ts"> import LoginForm from "../components/LoginForm.vue"; </script> <style scoped> .login-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #f0f2f5; padding: 20px; } .login-card { background: #fff; border-radius: 12px; padding: 40px; width: 100%; max-width: 420px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); } .login-title { text-align: center; color: #333; margin-bottom: 30px; font-size: 24px; font-weight: 600; } </style>
5.4.4. 更新根组件 修改 App.vue,引入登录页面:
📄 文件:src/App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template> <Login /> </template> <script setup lang="ts"> import Login from './views/Login.vue' </script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } </style>
5.4.5. 清理默认文件 删除 Vite 创建的默认文件,保持项目整洁:
1 2 3 4 5 rm src/components/HelloWorld.vueecho "" > src/style.css
5.5. 联调测试 代码编写完成,现在启动前后端服务进行联调测试。
5.5.1. 启动服务 启动后端服务
在后端项目目录下执行:
启动成功后,控制台会显示:
1 2 Started CaptchaApplication in x.xxx seconds 验证码资源加载完成
启动前端服务
在前端项目目录下执行:
启动成功后,终端会显示:
1 2 3 VITE v5.x.x ready in xxx ms ➜ Local: http://localhost:5173/
5.5.2. 功能验证 打开浏览器访问 http://localhost:5173,按以下步骤验证功能:
步骤 1:验证码生成
页面加载后,验证码组件会自动调用 /captcha/gen 接口获取验证码图片。观察:
步骤 2:验证码校验
拖动滑块完成验证,观察:
滑动到正确位置后,是否显示 “验证码已通过” 控制台是否输出校验成功的日志 步骤 3:登录流程
输入任意用户名和密码,点击登录按钮,观察:
是否成功调用 /captcha/login 接口 页面是否显示 “登录成功” 5.5.3. 常见问题排查 联调问题排查
2025-01-01 14:00
验证码图片显示不出来,控制台报 CORS 错误怎么办?
teacher
打开浏览器开发者工具,查看 ‘/captcha/check’ 请求的参数是否完整。重点检查 ‘bgImageWidth’ 和 ‘bgImageHeight’ 是否正确传递。
teacher
验证码有过期时间(默认 2 分钟)。如果验证码校验成功后等待太久才点登录,token 会过期。重新完成验证码验证即可。
问题速查表
现象 可能原因 解决方案 验证码图片不显示 CORS 未配置 检查后端 CorsConfig 验证码图片不显示 后端未启动 启动后端服务 校验一直失败 字段映射错误 检查 CaptchaCheckDTO 的字段转换 登录提示 token 失效 验证码过期 重新完成验证码验证 控制台报 404 接口路径错误 检查 API 地址是否正确
5.6. 本章小结 本章我们完成了前端项目的搭建和验证码组件的集成。从跨域配置到完整的登录流程,整个链路已经打通。
核心要点回顾
环节 关键点 跨域配置 后端通过 CorsFilter 允许前端地址访问 组件集成 使用 kite-captcha-vue 替代官方过时的 SDK API 封装 Axios 拦截器统一解包 Result<T> 结构 数据适配 前端组件返回的数据结构需要与后端 DTO 对应 二次验证 校验成功后保存 validToken,登录时携带
至此,Tianai-Captcha 的完整集成教程已经结束。你已经掌握了:
后端验证码服务的搭建与配置 多种验证码类型的动态切换 生产级配置(Redis 缓存、资源加载、二次验证) 前端组件的集成与联调