Spring Boot 3 滑动/旋转/滑动还原/文字点选验证码集成:Tianai-Captcha 快速接入指南

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>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Tianai-Captcha 核心依赖 -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<version>1.5.3</version>
</dependency>

<!-- Redis(可选,分布式环境必须) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Hutool 工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.25</version>
</dependency>


<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- Validation -->
<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-CaptchaAJ-Captcha
Spring Boot 3 支持✅ 原生支持❌ 需要手动适配
维护状态✅ 活跃维护⚠️ 更新较慢
包名兼容✅ 使用 jakarta.servlet❌ 使用 javax.servlet
UI 现代化✅ 现代化设计⚠️ 较为传统
集成难度✅ 开箱即用❌ 需要额外配置

image-20251231095254453


2.2. 四种验证码类型对比

Tianai-Captcha 支持四种验证码类型, 每种类型适用于不同的业务场景:

类型常量用户操作适用场景安全等级
滑动验证码SLIDER拖动滑块到缺口位置登录、注册、评论⭐⭐⭐
旋转验证码ROTATE旋转图片到正确角度支付、敏感操作⭐⭐⭐⭐
滑动还原验证码CONCAT拖动图片碎片拼接完整高安全场景⭐⭐⭐⭐
文字点选验证码WORD_IMAGE_CLICK按顺序点击指定文字防机器人、高安全⭐⭐⭐⭐⭐

选择建议

  • 普通场景(登录/注册):使用 SLIDER,用户体验最好
  • 支付场景:使用 ROTATECONCAT,安全性更高
  • 防刷场景:使用 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);
// ...
}

这样做的问题:

  1. 如果需要切换到 ROTATE 类型, 必须修改代码并重新部署
  2. 无法根据不同业务场景使用不同类型
  3. 测试环境和生产环境无法使用不同配置

正确做法:配置驱动

我们通过 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;
}

/**
* 根据 code 获取枚举
*/
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

# Tianai-Captcha 配置
captcha:
# 默认验证码类型(可动态切换)
default-type: SLIDER

# 启用的验证码类型列表
enabled-types:
- SLIDER
- ROTATE
- WORD_IMAGE_CLICK

# 各类型的过期时间(毫秒)
expire:
SLIDER: 120000 # 2分钟
ROTATE: 120000 # 2分钟
CONCAT: 120000 # 2分钟
WORD_IMAGE_CLICK: 180000 # 3分钟(文字点选需要更长时间)

# 二次验证配置
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

# Redis 缓存配置
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; // 格式: yyyy-MM-dd HH:mm:ss.SSS
endSlidingTime: string; // 格式: yyyy-MM-dd HH:mm:ss.SSS
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; // ❌ 不一致:前端是 sliderImageWidth
private Integer templateImageHeight; // ❌ 不一致:前端是 sliderImageHeight
private Long startTime; // ❌ 不一致:前端是 startSlidingTime (String)
private Long stopTime; // ❌ 不一致:前端是 endSlidingTime (String)
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;

/**
* 验证码校验请求对象
* 适配 kite-captcha-vue 组件的数据结构
*/
@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;
}

/**
* 转换为 Tianai-Captcha 原生格式
*/
public ImageCaptchaTrack toImageCaptchaTrack() {
// 字段映射规则:前端字段名 -> 后端字段名
CopyOptions options = CopyOptions.create()
.setFieldMapping(java.util.Map.of(
"sliderImageWidth", "templateImageWidth",
"sliderImageHeight", "templateImageHeight"
));

ImageCaptchaTrack track = BeanUtil.toBean(data, ImageCaptchaTrack.class, options);

// 时间格式转换:String -> Long
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 {

/**
* 生成验证码
*
* @param type 验证码类型(可选,不传则使用默认类型)
* @return 验证码数据
*/
ImageCaptchaVO generateCaptcha(String type);

/**
* 校验验证码
*
* @param checkDTO 校验请求
* @return 是否通过
*/
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) {
// 1. 确定验证码类型
String captchaType = determineCaptchaType(type);

// 2. 调用 Tianai-Captcha 生成验证码
ApiResponse<ImageCaptchaVO> response = imageCaptchaApplication.generateCaptcha(captchaType);

// 3. 检查生成是否成功
if (!response.isSuccess()) {
log.error("生成验证码失败: {}", response.getMsg());
throw new BizException("生成验证码失败");
}

// 4. 直接返回官方的 ImageCaptchaVO
ImageCaptchaVO captchaVO = response.getData();
log.info("生成验证码成功, id: {}, type: {}", captchaVO.getId(), captchaVO.getType());
return captchaVO;
}

@Override
public Map<String, Object> checkCaptcha(CaptchaCheckDTO checkDTO) {
// 1.通过我们封装好的DTO方法无缝转换为 Tianai-Captcha 的 ImageCaptchaTrack
ImageCaptchaTrack track = checkDTO.toImageCaptchaTrack();

// 2. 调用 Tianai-Captcha 校验验证码
ApiResponse<?> response = imageCaptchaApplication.matching(checkDTO.getId(), track);

// 3. 检查校验结果
if (!response.isSuccess()) {
log.warn("验证码校验失败, id: {}, msg: {}", checkDTO.getId(), response.getMsg());
throw new BizException(ResultCode.CAPTCHA_ERROR.getCode(), "验证码校验失败");
}

log.info("验证码校验成功, id: {}", checkDTO.getId());

// 4. 返回验证码 ID 作为 validToken(用于二次验证)
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("validToken", checkDTO.getId()); // ⚠️ 直接返回验证码 ID

return result;
}

/**
* 确定验证码类型
*
* @param type 前端传入的类型(可能为空)
* @return 最终使用的验证码类型
*/
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;

/**
* 生成验证码
*
* @param type 验证码类型(可选)
* SLIDER - 滑动验证码
* ROTATE - 旋转验证码
* CONCAT - 滑动还原验证码
* WORD_IMAGE_CLICK - 文字点选验证码
* @return 验证码数据
*/
/**
* 生成验证码
*
* @param type 验证码类型(可选,可以从URL参数或请求体中获取)
* SLIDER - 滑动验证码
* ROTATE - 旋转验证码
* CONCAT - 滑动还原验证码
* WORD_IMAGE_CLICK - 文字点选验证码
* @return 验证码数据
*/
@PostMapping("/gen")
public Result<ImageCaptchaVO> generateCaptcha(
@RequestParam(required = false) String type,
@RequestBody(required = false) Map<String, Object> body) {
log.info("生成验证码请求, type: {}", type);

// 如果URL参数没有type,尝试从请求体中获取
if (type == null && body != null && body.containsKey("type")) {
type = (String) body.get("type");
}

ImageCaptchaVO imageCaptchaVO = captchaService.generateCaptcha(type);
return Result.success(imageCaptchaVO);
}

/**
* 校验验证码
*
* @param checkDTO 校验请求
* @return 校验结果
*/
@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×360JPG用于展示验证码的背景
滑块模板(active.png)110×110PNG(透明)滑块图片
滑块模板(fixed.png)110×110PNG(透明)凹槽图片
旋转模板(active.png)200×200PNG(透明)旋转图片
旋转模板(fixed.png)200×200PNG(透明)旋转凹槽

三种资源加载方式对比

加载方式适用场景优点缺点
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 = (CrudResourceStore) resourceStore;

// 1. 添加滑动验证码模板
addSliderTemplates(crudResourceStore);

// 2. 添加旋转验证码模板
addRotateTemplates(crudResourceStore);

// 3. 添加背景图片
addBackgroundImages(crudResourceStore);

log.info("验证码资源加载完成");
}

/**
* 添加滑动验证码模板
*/
private void addSliderTemplates(CrudResourceStore crudResourceStore) {
// 模板 1
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"));

// 模板 2
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 个模板");
}

/**
* 添加背景图片
* 支持三种加载方式:
* 1. classpath - 从类路径加载(打包在 JAR 内)
* 2. file - 从本地文件系统加载
* 3. url - 从远程 URL 加载(如 OSS/CDN)
*/
private void addBackgroundImages(CrudResourceStore crudResourceStore) {
// 方式 1:从 classpath 加载(推荐用于开发环境)
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"));

// 方式 2:从本地文件系统加载(推荐用于生产环境)
// crudResourceStore.addResource(CaptchaTypeConstant.SLIDER,
// new Resource("file", "/data/captcha/backgrounds/1.png", "default"));

// 方式 3:从远程 URL 加载(推荐用于分布式环境)
// crudResourceStore.addResource(CaptchaTypeConstant.SLIDER,
// new Resource("url", "https://cdn.example.com/captcha/1.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 张图片");
}
}

注意事项

  1. 如果使用 classpath 方式,资源文件必须放在 src/main/resources 目录下
  2. 如果使用 file 方式,确保服务器有读取文件的权限
  3. 如果使用 url 方式,确保网络连接稳定,建议使用 CDN 加速

关于资源可以自取,将以下的压缩包下载后将 silider_resources 改为 resources 粘贴至 Java 资源文件夹即可

https://prorise-blog.oss-cn-guangzhou.aliyuncs.com/博客文件存储/silider_resources.rar


4.2. Redis 缓存集成

为什么需要 Redis?

在分布式环境下,验证码数据需要在多个服务实例之间共享。如果使用本地内存缓存,会导致以下问题:

  1. 验证码无法跨实例校验:用户在实例 A 生成验证码,请求被负载均衡到实例 B 进行校验,会提示验证码不存在
  2. 无法实现二次验证:二次验证需要在校验成功后生成 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
redis-cli

# 查看所有验证码相关的 key
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-100ms5-10ms10 倍
并发 100 QPSCPU 占用 30%CPU 占用 5%6 倍

4.4. 二次验证机制

什么是二次验证?

二次验证是指在验证码校验成功后,生成一个临时 token 返回给前端,前端在后续的业务请求中携带这个 token,后端通过校验 token 来确认用户已经通过了验证码验证。

为什么需要二次验证?

假设我们的登录流程是这样的:

  1. 前端调用 /captcha/gen 生成验证码
  2. 用户完成验证码,前端调用 /captcha/check 校验验证码
  3. 校验成功后,前端调用 /user/login 进行登录

在这个流程中,如果没有二次验证,攻击者可以:

  1. 自己完成验证码验证
  2. 拿到验证码 ID
  3. 使用这个 ID 进行暴力破解登录接口

有了二次验证后,流程变为:

  1. 前端调用 /captcha/gen 生成验证码
  2. 用户完成验证码,前端调用 /captcha/check 校验验证码
  3. 校验成功后,后端返回一个临时 token
  4. 前端调用 /user/login 时携带这个 token
  5. 后端校验 token 是否有效,有效则允许登录

开启二次验证

application.yml 中开启二次验证:

1
2
3
4
5
captcha:
# 开启二次验证
secondary-enabled: true
# 二次验证 token 过期时间(毫秒)
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) {
// 1. 调用 Tianai-Captcha 校验验证码
ApiResponse<?> response = imageCaptchaApplication.matching(checkDTO.getId(), checkDTO.getTrack());

// 2. 检查校验结果
if (!response.isSuccess()) {
log.warn("验证码校验失败, id: {}, msg: {}", checkDTO.getId(), response.getMsg());
throw new BizException(ResultCode.CAPTCHA_ERROR.getCode(), "验证码校验失败");
}

log.info("验证码校验成功, id: {}", checkDTO.getId());

// 3. 返回验证码 ID 作为 validToken(用于二次验证)
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("validToken", checkDTO.getId()); // ⚠️ 直接返回验证码 ID

return result;
}

/**
* 二次验证(新增方法)
* 在业务接口中调用,验证用户是否已通过验证码校验
*
* @param validToken 验证码 ID(校验成功后返回的 token)
* @return 是否通过
*/
public boolean secondaryVerification(String validToken) {
if (!captchaProperties.isSecondaryEnabled()) {
throw new BizException("二次验证未开启");
}

// 调用 Tianai-Captcha 的二次验证接口
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
// ... 省略其他代码

/**
* 校验验证码
*
* @param checkDTO 校验请求
* @return 校验结果(包含 success 和 token)
*/
Map<String, Object> checkCaptcha(CaptchaCheckDTO checkDTO); // ⚠️ 返回值从 boolean 改为 Map

/**
* 二次验证(新增方法)
*
* @param validToken 二次验证 token
* @return 是否通过
*/
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
/**
* 模拟登录接口(演示二次验证的使用)
*
* @param validToken 验证码校验成功后返回的 token
* @param username 用户名
* @param password 密码
* @return 登录结果
*/
@PostMapping("/login")
public Result<?> login(@RequestParam String validToken,
@RequestParam String username,
@RequestParam String password) {
// 1. 先进行二次验证
captchaService.secondaryVerification(validToken);

// 2. 二次验证通过后,执行业务逻辑
// TODO: 实际的登录逻辑
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.addAllowedOrigin("http://localhost:3000");

// 允许携带 Cookie(如果需要)
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-frontend

# 安装依赖
pnpm install

执行完成后,可以先启动项目确认环境正常:

1
pnpm dev

浏览器访问 http://localhost:5173,看到 Vite + Vue 的欢迎页面即表示项目创建成功。

5.2.2. 安装核心依赖

我们需要安装以下依赖:

1
2
3
4
5
6
7
# 验证码组件
pnpm add kite-captcha-vue

# i18N 库
pnpm add vue-i18n
# HTTP 请求库
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
}

/**
* 验证码生成响应(对应后端 ImageCaptchaVO)
*/
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'

// 创建 Axios 实例
const request = axios.create({
baseURL: 'http://localhost:8080',
timeout: 10000
})

// 响应拦截器:统一解包 Result 结构
request.interceptors.response.use(
(response) => {
const result = response.data as Result<unknown>

// 业务成功,直接返回 data
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'

/**
* 生成验证码
* @param type 验证码类型(可选)
*/
export function generateCaptcha(type?: string): Promise<CaptchaData> {
return request.post('/captcha/gen', { type })
}

/**
* 校验验证码
* @param data 校验数据(由 kite-captcha-vue 组件提供)
*/
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-modelnumber or string-v-model 绑定值,用于接收验证结果状态码
scalenumber1缩放比例
loadingAnimationstring‘slide’加载动画类型,可选值:'slide' 'bounce' 'fade' 'lightSpeed' 'zoom'
showClosebooleantrue是否显示关闭按钮
backgroundImagestring-背景图片
renderDataApiFunction-获取验证码数据的异步函数
matchingResultCodePropsMap验证结果状态码配置 expired 表示已过期 failed 表示验证失败 success 表示验证成功

对外暴露的方法

方法名参数描述
refresh-重新加载验证码

事件

事件名参数描述
render-successboolean验证码渲染成功回调
matching-dataCaptchaMatchingData返回验证轨迹数据

📄 文件: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.vue

# 清空默认样式
echo "" > src/style.css

5.5. 联调测试

代码编写完成,现在启动前后端服务进行联调测试。

5.5.1. 启动服务

启动后端服务

在后端项目目录下执行:

1
2
3
4
5
# Maven 方式
mvn spring-boot:run

# 或者直接运行启动类
# 在 IDEA 中运行 CaptchaApplication.java

启动成功后,控制台会显示:

1
2
Started CaptchaApplication in x.xxx seconds
验证码资源加载完成

启动前端服务

在前端项目目录下执行:

1
pnpm dev

启动成功后,终端会显示:

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
S

验证码图片显示不出来,控制台报 CORS 错误怎么办?

T
teacher

检查后端的 CorsConfig 是否正确配置了前端地址 ‘http://localhost: 5173’,然后重启后端服务。

S

验证码校验一直失败,提示’验证码错误’?

T
teacher

打开浏览器开发者工具,查看 ‘/captcha/check’ 请求的参数是否完整。重点检查 ‘bgImageWidth’ 和 ‘bgImageHeight’ 是否正确传递。

S

登录时提示’验证码已失效’?

T
teacher

验证码有过期时间(默认 2 分钟)。如果验证码校验成功后等待太久才点登录,token 会过期。重新完成验证码验证即可。

问题速查表

现象可能原因解决方案
验证码图片不显示CORS 未配置检查后端 CorsConfig
验证码图片不显示后端未启动启动后端服务
校验一直失败字段映射错误检查 CaptchaCheckDTO 的字段转换
登录提示 token 失效验证码过期重新完成验证码验证
控制台报 404接口路径错误检查 API 地址是否正确

5.6. 本章小结

本章我们完成了前端项目的搭建和验证码组件的集成。从跨域配置到完整的登录流程,整个链路已经打通。

核心要点回顾

环节关键点
跨域配置后端通过 CorsFilter 允许前端地址访问
组件集成使用 kite-captcha-vue 替代官方过时的 SDK
API 封装Axios 拦截器统一解包 Result<T> 结构
数据适配前端组件返回的数据结构需要与后端 DTO 对应
二次验证校验成功后保存 validToken,登录时携带

至此,Tianai-Captcha 的完整集成教程已经结束。你已经掌握了:

  1. 后端验证码服务的搭建与配置
  2. 多种验证码类型的动态切换
  3. 生产级配置(Redis 缓存、资源加载、二次验证)
  4. 前端组件的集成与联调