基于SpringBoot实现图片滑动验证码功能
一、功能原理阐述
1.1 滑动验证码的工作原理
滑动验证码是一种基于行为验证的人机识别机制,其核心思想是通过用户的交互行为来区分真人操作与自动化程序。整个验证流程包括以下几个关键步骤:
图像生成阶段
- 系统从预设图片库中随机选择一张背景图
- 在背景图上随机选择一个位置,通过预定义的mask模板生成缺口形状
- 将缺口位置的图像区域裁剪出来作为滑块拼图
- 对背景图的缺口位置进行半透明或模糊处理,形成明显的视觉提示
用户交互阶段
- 前端展示带有缺口的背景图和对应的滑块拼图
- 用户通过鼠标拖拽或手指滑动,将滑块移动到正确位置
- 系统实时记录用户的滑动轨迹、速度、加速度等行为数据
验证判断阶段
- 用户释放滑块后,前端收集最终的位移坐标和完整轨迹数据
- 将数据提交到后端进行验证
- 后端对比滑块位置与实际缺口位置的匹配度
- 结合行为特征(轨迹平滑度、滑动时间、加速度变化等)综合判断
1.2 安全价值分析
滑动验证码相比传统验证码具有显著的安全优势:
防爬虫能力
- 传统字符验证码容易被OCR技术自动识别
- 滑动验证码要求用户进行复杂的空间定位操作,增加自动化难度
- 引入行为轨迹分析,机器人难以模拟真实用户的操作特征
防暴力 破解
- 每次验证都生成随机缺口位置,防止预先破解
- 图像库多样化,避免固定模式被机器学习模型识别
- 设置合理的容错范围,在用户体验和安全性之间取得平衡
防重放攻击
- 验证令牌具有时效性,过期即失效
- 结合用户会话信息,防止验证码被跨会话使用
- 前端加密传输,防止中间人攻击
1.3 应用场景
滑动验证码适用于多种安全敏感的业务场景:
用户认证场景
- 用户登录:防止恶意暴力 破解密码
- 用户注册:拦截批量虚假账号注册
- 密码找回:保护用户账号安全
业务操作场景
- 发表评论:防止恶意刷 评论、垃圾信息
- 内容发布:防止自动化内容提交
- 抢购活动:防止机器人抢购商品
- 表单提交:保护重要业务表单
数据保护场景
- 敏感信息查询:防止恶意数据爬取
- 导出功能:防止批量数据导出
- API接口调用:保护接口不被滥用
二、技术选型说明
2.1 核心技术栈
基于Java生态体系,我们选择以下技术栈实现滑动验证码功能:
| 技术组件 | 选型 | 选择理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.7+ | 快速开发,生态完善,配置简化 |
| 图像处理 | Java BufferedImage + 自定义算法 | 无需额外依赖,轻量高效 |
| 缓存存储 | Redis | 分布式部署支持,高性能 |
| 前端技术 | HTML5 + Canvas + JavaScript | 原生实现,兼容性好,无框架依赖 |
| 加密算法 | AES-128 | 对称加密,性能好,满足安全需求 |
| 数据格式 | JSON | 标准格式,前后端交互友好 |
2.2 技术选型理由详解
Java BufferedImage
- JDK自带,无需引入第三方图像处理库
- 提供丰富的图像操作API:裁剪、缩放、像素级操作
- 支持多种图像格式:PNG、JPEG等
- 内存占用合理,适合Web应用场景
Redis缓存
- 滑动验证码需要存储验证状态和临时数据
- Redis提供高性能的key-value存储
- 支持过期时间设置,自动清理过期验证码
- 分布式环境下保证多节点数据一致性
Canvas前端绘图
- HTML5原生支持,无需插件
- 可以动态绘制背景图和滑块
- 实现拖拽交互响应流畅
- 移动端和PC端统一实现
AES加密
- 滑动坐标等敏感信息需要加密传输
- AES算法成熟稳定,性能优异
- 防止客户端篡改验证数据
- Java提供标准加密库支持
三、实现步骤详解
3.1 项目环境搭建与依赖配置
创建Spring Boot项目
使用Spring Initializr创建新项目,选择以下依赖:
- Spring Web
- Spring Data Redis
- Lombok(可选,简化代码)
配置pom.xml依赖
<?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>2.7.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>slider-captcha</artifactId>
<version>1.0.0</version>
<name>slider-captcha</name>
<description>Spring Boot Slider Captcha</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml配置文件
server:
port: 8080
servlet:
context-path: /captcha
spring:
# Redis配置
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 3000
lettuce:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
# 验证码配置
captcha:
# 图片配置
image:
width: 320
height: 160
slider-width: 60
slider-height: 60
tolerance: 5 # 容错像素
# 过期时间(秒)
expire-time: 300
# 图片资源路径
resource-path: classpath:images/
# 加密密钥
secret-key: slider_captcha_secret_key_123456
3.2 核心算法实现
3.2.1 图像处理工具类
创建图片处理工具类,实现滑块验证码的核心图像处理功能:
package com.example.slidercaptcha.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;
/**
* 滑块验证码图片处理工具类
*/
public class CaptchaImageUtil {
private static final Random RANDOM = new Random();
// 滑块圆角半径
private static final int ARC_RADIUS = 10;
/**
* 生成验证码图片
*
* @param originalImage 原始背景图
* @param x 缺口x坐标
* @param y 缺口y坐标
* @param width 滑块宽度
* @param height 滑块高度
* @return 验证码结果
*/
public static CaptchaResult generateCaptcha(BufferedImage originalImage,
int x, int y,
int width, int height) {
try {
// 1. 创建带缺口的背景图
BufferedImage backgroundImage = createBackgroundWithHole(
originalImage, x, y, width, height);
// 2. 创建滑块图片
BufferedImage sliderImage = createSliderImage(
originalImage, x, y, width, height);
// 3. 转换为Base64
String backgroundBase64 = imageToBase64(backgroundImage);
String sliderBase64 = imageToBase64(sliderImage);
return new CaptchaResult(x, y, backgroundBase64, sliderBase64);
} catch (Exception e) {
throw new RuntimeException("生成验证码图片失败", e);
}
}
/**
* 创建带缺口的背景图
*/
private static BufferedImage createBackgroundWithHole(
BufferedImage originalImage, int x, int y, int width, int height) {
BufferedImage newImage = new BufferedImage(
originalImage.getWidth(),
originalImage.getHeight(),
BufferedImage.TYPE_INT_RGB
);
Graphics2D g2d = newImage.createGraphics();
// 绘制原图
g2d.drawImage(originalImage, 0, 0, null);
// 设置半透明填充
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
g2d.setColor(new Color(0, 0, 0, 100));
// 绘制缺口区域
drawSliderShape(g2d, x, y, width, height);
g2d.fill(new RoundRectangle2D.Double(x, y, width, height, ARC_RADIUS, ARC_RADIUS));
// 绘制缺口边框
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
g2d.setColor(new Color(255, 255, 255, 200));
g2d.setStroke(new BasicStroke(2f));
drawSliderShape(g2d, x, y, width, height);
g2d.dispose();
return newImage;
}
/**
* 创建滑块图片
*/
private static BufferedImage createSliderImage(
BufferedImage originalImage, int x, int y, int width, int height) {
BufferedImage sliderImage = new BufferedImage(
width, height, BufferedImage.TYPE_INT_ARGB
);
Graphics2D g2d = sliderImage.createGraphics();
// 启用抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// 裁剪滑块区域
BufferedImage croppedImage = originalImage.getSubimage(x, y, width, height);
// 绘制圆角滑块
drawRoundedSlider(g2d, croppedImage, width, height);
g2d.dispose();
return sliderImage;
}
/**
* 绘制圆角滑块
*/
private static void drawRoundedSlider(Graphics2D g2d,
BufferedImage image,
int width, int height) {
// 创建圆角形状
RoundRectangle2D roundRect = new RoundRectangle2D.Double(
0, 0, width, height, ARC_RADIUS, ARC_RADIUS
);
g2d.setClip(roundRect);
g2d.drawImage(image, 0, 0, null);
// 绘制边框
g2d.setColor(new Color(255, 255, 255));
g2d.setStroke(new BasicStroke(2f));
g2d.draw(roundRect);
}
/**
* 绘制滑块形状
*/
private static void drawSliderShape(Graphics2D g2d,
int x, int y,
int width, int height) {
RoundRectangle2D shape = new RoundRectangle2D.Double(
x, y, width, height, ARC_RADIUS, ARC_RADIUS
);
g2d.draw(shape);
}
/**
* 图片转Base64
*/
private static String imageToBase64(BufferedImage image) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
byte[] imageBytes = baos.toByteArray();
return "data:image/png;base64," +
java.util.Base64.getEncoder().encodeToString(imageBytes);
}
/**
* 从资源目录加载图片
*/
public static BufferedImage loadImageFromClasspath(String path) throws IOException {
InputStream inputStream = CaptchaImageUtil.class
.getClassLoader()
.getResourceAsStream(path);
if (inputStream == null) {
throw new IOException("无法加载图片: " + path);
}
return ImageIO.read(inputStream);
}
/**
* 验证码结果类
*/
public static class CaptchaResult {
private final int x; // 缺口x坐标
private final int y; // 缺口y坐标
private final String backgroundImage; // 背景图Base64
private final String sliderImage; // 滑块图Base64
public CaptchaResult(int x, int y, String backgroundImage, String sliderImage) {
this.x = x;
this.y = y;
this.backgroundImage = backgroundImage;
this.sliderImage = sliderImage;
}
public int getX() { return x; }
public int getY() { return y; }
public String getBackgroundImage() { return backgroundImage; }
public String getSliderImage() { return sliderImage; }
}
}
3.2.2 加密工具类
package com.example.slidercaptcha.util;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* AES加密工具类
*/
public class AESUtil {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
/**
* 加密
*/
public static String encrypt(String content, String key) throws Exception {
// 处理密钥,确保16位
byte[] keyBytes = padKey(key.getBytes(StandardCharsets.UTF_8));
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
IvParameterSpec ivParameterSpec = new IvParameterSpec(keyBytes);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* 解密
*/
public static String decrypt(String encrypted, String key) throws Exception {
// 处理密钥,确保16位
byte[] keyBytes = padKey(key.getBytes(StandardCharsets.UTF_8));
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
IvParameterSpec ivParameterSpec = new IvParameterSpec(keyBytes);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encrypted));
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* 填充密钥到16位
*/
private static byte[] padKey(byte[] key) {
byte[] result = new byte[16];
System.arraycopy(key, 0, result, 0, Math.min(key.length, 16));
return result;
}
}
3.2.3 验证码服务类
package com.example.slidercaptcha.service;
import com.example.slidercaptcha.config.CaptchaProperties;
import com.example.slidercaptcha.util.AESUtil;
import com.example.slidercaptcha.util.CaptchaImageUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 验证码服务类
*/
@Slf4j
@Service
public class CaptchaService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private CaptchaProperties captchaProperties;
private static final Random RANDOM = new Random();
private static final String CAPTCHA_PREFIX = "captcha:";
private static final String VERIFY_PREFIX = "verify:";
/**
* 生成验证码
*/
public CaptchaResponse generate() {
try {
// 1. 生成验证码token
String token = UUID.randomUUID().toString().replace("-", "");
// 2. 加载随机背景图
BufferedImage originalImage = loadRandomBackgroundImage();
// 3. 随机生成滑块位置
int width = captchaProperties.getImage().getWidth();
int height = captchaProperties.getImage().getHeight();
int sliderWidth = captchaProperties.getImage().getSliderWidth();
int sliderHeight = captchaProperties.getImage().getSliderHeight();
// x坐标范围:[sliderWidth, width - sliderWidth]
int maxX = width - sliderWidth - captchaProperties.getImage().getTolerance();
int minX = sliderWidth + captchaProperties.getImage().getTolerance();
int x = RANDOM.nextInt(maxX - minX) + minX;
// y坐标范围:[0, height - sliderHeight]
int maxY = height - sliderHeight;
int y = RANDOM.nextInt(maxY);
// 4. 生成验证码图片
CaptchaImageUtil.CaptchaResult captchaResult =
CaptchaImageUtil.generateCaptcha(
originalImage, x, y, sliderWidth, sliderHeight
);
// 5. 加密滑块位置信息
String positionData = x + "," + y;
String encryptedData = AESUtil.encrypt(positionData,
captchaProperties.getSecretKey());
// 6. 存储验证信息到Redis
String captchaKey = CAPTCHA_PREFIX + token;
redisTemplate.opsForValue().set(
captchaKey,
encryptedData,
captchaProperties.getExpireTime(),
TimeUnit.SECONDS
);
// 7. 返回结果
return CaptchaResponse.builder()
.token(token)
.backgroundImage(captchaResult.getBackgroundImage())
.sliderImage(captchaResult.getSliderImage())
.width(width)
.height(height)
.sliderWidth(sliderWidth)
.sliderHeight(sliderHeight)
.build();
} catch (Exception e) {
log.error("生成验证码失败", e);
throw new RuntimeException("生成验证码失败", e);
}
}
/**
* 验证滑块位置
*/
public boolean verify(String token, int moveX, int moveY) {
try {
// 1. 从Redis获取验证信息
String captchaKey = CAPTCHA_PREFIX + token;
String encryptedData = redisTemplate.opsForValue().get(captchaKey);
if (encryptedData == null) {
log.warn("验证码已过期或不存在: {}", token);
return false;
}
// 2. 解密获取正确位置
String positionData = AESUtil.decrypt(encryptedData,
captchaProperties.getSecretKey());
String[] positions = positionData.split(",");
int correctX = Integer.parseInt(positions[0]);
int correctY = Integer.parseInt(positions[1]);
// 3. 验证位置误差
int tolerance = captchaProperties.getImage().getTolerance();
boolean xValid = Math.abs(moveX - correctX) <= tolerance;
boolean yValid = Math.abs(moveY - correctY) <= tolerance;
// 4. 删除验证码(一次性使用)
redisTemplate.delete(captchaKey);
if (xValid && yValid) {
// 5. 生成验证通过令牌
String verifyToken = UUID.randomUUID().toString().replace("-", "");
String verifyKey = VERIFY_PREFIX + verifyToken;
redisTemplate.opsForValue().set(
verifyKey,
"verified",
300,
TimeUnit.SECONDS
);
log.info("验证码验证成功: token={}, verifyToken={}", token, verifyToken);
return true;
} else {
log.warn("验证码验证失败: token={}, moveX={}, moveY={}, correctX={}, correctY={}",
token, moveX, moveY, correctX, correctY);
return false;
}
} catch (Exception e) {
log.error("验证码验证异常", e);
return false;
}
}
/**
* 二次校验(用于业务接口验证)
*/
public boolean verifyToken(String verifyToken) {
try {
String verifyKey = VERIFY_PREFIX + verifyToken;
String value = redisTemplate.opsForValue().get(verifyKey);
if (value != null) {
// 删除令牌(一次性使用)
redisTemplate.delete(verifyKey);
return true;
}
return false;
} catch (Exception e) {
log.error("验证令牌校验异常", e);
return false;
}
}
/**
* 加载随机背景图
*/
private BufferedImage loadRandomBackgroundImage() throws Exception {
String resourcePath = captchaProperties.getResourcePath();
String[] imageFiles = {
"bg1.jpg", "bg2.jpg", "bg3.jpg",
"bg4.jpg", "bg5.jpg"
};
int index = RANDOM.nextInt(imageFiles.length);
String imagePath = resourcePath + imageFiles[index];
return CaptchaImageUtil.loadImageFromClasspath(imagePath);
}
}
3.3 前后端交互设计
3.3.1 Controller层实现
package com.example.slidercaptcha.controller;
import com.example.slidercaptcha.service.CaptchaService;
import com.example.slidercaptcha.vo.CaptchaRequest;
import com.example.slidercaptcha.vo.CaptchaResponse;
import com.example.slidercaptcha.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 验证码控制器
*/
@Slf4j
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class CaptchaController {
@Autowired
private CaptchaService captchaService;
/**
* 生成验证码
*/
@GetMapping("/captcha/generate")
public Result<CaptchaResponse> generate() {
try {
CaptchaResponse response = captchaService.generate();
return Result.success(response);
} catch (Exception e) {
log.error("生成验证码失败", e);
return Result.error("生成验证码失败");
}
}
/**
* 验证滑块位置
*/
@PostMapping("/captcha/verify")
public Result<String> verify(@RequestBody CaptchaRequest request) {
try {
boolean success = captchaService.verify(
request.getToken(),
request.getMoveX(),
request.getMoveY()
);
if (success) {
// 返回验证通过令牌
String verifyToken = java.util.UUID.randomUUID()
.toString().replace("-", "");
return Result.success(verifyToken, "验证成功");
} else {
return Result.error("验证失败");
}
} catch (Exception e) {
log.error("验证码校验失败", e);
return Result.error("验证失败");
}
}
/**
* 二次校验接口(业务接口调用)
*/
@PostMapping("/captcha/check")
public Result<Void> check(@RequestParam String verifyToken) {
try {
boolean success = captchaService.verifyToken(verifyToken);
if (success) {
return Result.success(null, "校验通过");
} else {
return Result.error("校验失败");
}
} catch (Exception e) {
log.error("二次校验失败", e);
return Result.error("校验失败");
}
}
}
3.3.2 VO类定义
package com.example.slidercaptcha.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 验证码响应
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CaptchaResponse {
private String token; // 验证码令牌
private String backgroundImage; // 背景图Base64
private String sliderImage; // 滑块图Base64
private int width; // 背景图宽度
private int height; // 背景图高度
private int sliderWidth; // 滑块宽度
private int sliderHeight; // 滑块高度
}
/**
* 验证码请求
*/
@Data
public class CaptchaRequest {
private String token; // 验证码令牌
private int moveX; // 滑块X轴移动距离
private int moveY; // 滑块Y轴移动距离
private String behavior; // 行为轨迹(可选)
}
/**
* 通用结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> success(T data, String message) {
return new Result<>(200, message, data);
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null);
}
}
3.3.3 配置属性类
package com.example.slidercaptcha.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 验证码配置属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "captcha")
public class CaptchaProperties {
private ImageConfig image = new ImageConfig();
private int expireTime = 300;
private String resourcePath = "classpath:images/";
private String secretKey = "slider_captcha_secret_key_123456";
@Data
public static class ImageConfig {
private int width = 320;
private int height = 160;
private int sliderWidth = 60;
private int sliderHeight = 60;
private int tolerance = 5;
}
}
3.4 前端页面实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>滑动验证码</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.captcha-container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.captcha-title {
text-align: center;
margin-bottom: 20px;
color: #333;
font-size: 18px;
}
.image-wrapper {
position: relative;
width: 320px;
height: 160px;
border: 2px solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
background: #f0f0f0;
}
.background-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.slider-image {
position: absolute;
width: 60px;
height: 60px;
top: 0;
left: 0;
cursor: grab;
user-select: none;
}
.slider-image:active {
cursor: grabbing;
}
.slider-track {
width: 320px;
height: 40px;
background: #f0f0f0;
border: 1px solid #e0e0e0;
border-radius: 4px;
margin-top: 10px;
position: relative;
}
.slider-btn {
width: 50px;
height: 38px;
background: linear-gradient(90deg, #4CAF50, #45a049);
border-radius: 3px;
position: absolute;
left: 0;
top: 0;
cursor: grab;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 20px;
user-select: none;
}
.slider-btn:active {
cursor: grabbing;
}
.slider-text {
text-align: center;
line-height: 40px;
color: #999;
font-size: 14px;
pointer-events: none;
}
.refresh-btn {
width: 100%;
padding: 10px;
margin-top: 10px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.refresh-btn:hover {
background: #0b7dda;
}
.result-message {
text-align: center;
margin-top: 10px;
font-size: 14px;
padding: 8px;
border-radius: 4px;
display: none;
}
.success {
background: #dff0d8;
color: #3c763d;
display: block !important;
}
.error {
background: #f2dede;
color: #a94442;
display: block !important;
}
.loading {
text-align: center;
padding: 20px;
color: #999;
}
</style>
</head>
<body>
<div class="captcha-container">
<h2 class="captcha-title">滑动验证码</h2>
<div id="captcha-content">
<div class="image-wrapper">
<img id="background-image" class="background-image" src="" alt="背景图">
<img id="slider-image" class="slider-image" src="" alt="滑块">
</div>
<div class="slider-track">
<div id="slider-btn" class="slider-btn">→</div>
<div class="slider-text">拖动滑块完成验证</div>
</div>
<button class="refresh-btn" onclick="loadCaptcha()">刷新验证码</button>
<div id="result-message" class="result-message"></div>
</div>
<div id="loading" class="loading" style="display: none;">
加载中...
</div>
</div>
<script>
let currentToken = null;
let sliderPosition = 0;
let isDragging = false;
let startX = 0;
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
loadCaptcha();
initSliderEvents();
});
// 加载验证码
async function loadCaptcha() {
showLoading(true);
try {
const response = await fetch('/captcha/api/captcha/generate');
const result = await response.json();
if (result.code === 200) {
const data = result.data;
currentToken = data.token;
document.getElementById('background-image').src = data.backgroundImage;
document.getElementById('slider-image').src = data.sliderImage;
document.getElementById('slider-image').style.top = '0px';
document.getElementById('slider-image').style.left = '0px';
sliderPosition = 0;
hideMessage();
} else {
showMessage('加载验证码失败', 'error');
}
} catch (error) {
console.error('加载验证码失败:', error);
showMessage('网络错误', 'error');
}
showLoading(false);
}
// 初始化滑块事件
function initSliderEvents() {
const sliderBtn = document.getElementById('slider-btn');
// 鼠标事件
sliderBtn.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', endDrag);
// 触摸事件
sliderBtn.addEventListener('touchstart', startDragTouch);
document.addEventListener('touchmove', dragTouch);
document.addEventListener('touchend', endDrag);
}
// 开始拖拽(鼠标)
function startDrag(e) {
isDragging = true;
startX = e.clientX;
e.preventDefault();
}
// 开始拖拽(触摸)
function startDragTouch(e) {
isDragging = true;
startX = e.touches[0].clientX;
e.preventDefault();
}
// 拖拽过程(鼠标)
function drag(e) {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const maxMove = 320 - 60; // 容器宽度 - 滑块宽度
sliderPosition = Math.max(0, Math.min(maxMove, deltaX));
updateSliderPosition();
}
// 拖拽过程(触摸)
function dragTouch(e) {
if (!isDragging) return;
const deltaX = e.touches[0].clientX - startX;
const maxMove = 320 - 60;
sliderPosition = Math.max(0, Math.min(maxMove, deltaX));
updateSliderPosition();
e.preventDefault();
}
// 更新滑块位置
function updateSliderPosition() {
document.getElementById('slider-btn').style.left = sliderPosition + 'px';
document.getElementById('slider-image').style.left = sliderPosition + 'px';
}
// 结束拖拽
function endDrag() {
if (!isDragging) return;
isDragging = false;
verifyCaptcha();
}
// 验证验证码
async function verifyCaptcha() {
if (!currentToken) {
showMessage('验证码已过期', 'error');
return;
}
try {
const response = await fetch('/captcha/api/captcha/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: currentToken,
moveX: Math.round(sliderPosition),
moveY: 0
})
});
const result = await response.json();
if (result.code === 200) {
showMessage('验证成功!', 'success');
console.log('验证令牌:', result.data);
} else {
showMessage('验证失败,请重试', 'error');
setTimeout(loadCaptcha, 1500);
}
} catch (error) {
console.error('验证失败:', error);
showMessage('网络错误', 'error');
}
}
// 显示消息
function showMessage(message, type) {
const msgEl = document.getElementById('result-message');
msgEl.textContent = message;
msgEl.className = 'result-message ' + type;
}
// 隐藏消息
function hideMessage() {
document.getElementById('result-message').style.display = 'none';
}
// 显示/隐藏加载状态
function showLoading(show) {
document.getElementById('captcha-content').style.display = show ? 'none' : 'block';
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
</script>
</body>
</html>
四、完整代码展示
4.1 后端核心代码
4.1.1 主启动类
package com.example.slidercaptcha;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SliderCaptchaApplication {
public static void main(String[] args) {
SpringApplication.run(SliderCaptchaApplication.class, args);
}
}
4.1.2 Redis配置类
package com.example.slidercaptcha.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
serializer.setObjectMapper(mapper);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
4.2 配置文件
完整的application.yml配置:
server:
port: 8080
servlet:
context-path: /captcha
spring:
application:
name: slider-captcha
# Redis配置
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 3000
lettuce:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
# 验证码配置
captcha:
# 图片配置
image:
width: 320
height: 160
slider-width: 60
slider-height: 60
tolerance: 5
# 过期时间(秒)
expire-time: 300
# 图片资源路径
resource-path: classpath:images/
# 加密密钥
secret-key: slider_captcha_secret_key_123456
# 日志配置
logging:
level:
com.example.slidercaptcha: debug
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
4.3 依赖配置
完整的pom.xml文件已在3.1节提供,此处补充主要依赖版本信息:
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.7.14</spring-boot.version>
<fastjson.version>1.2.83</fastjson.version>
<lombok.version>1.18.26</lombok.version>
</properties>
五、效果演示与使用说明
5.1 最终实现效果
完成上述实现后,滑动验证码功能将具备以下特性:
视觉效果
- 背景图:320x160像素的高质量图片
- 滑块图:60x60像素的圆角矩形拼图
- 缺口效果:半透明黑色遮罩,白色边框高亮
- 滑动条:绿色渐变按钮,拖动流畅
交互体验
- 鼠标拖拽:支持PC端鼠标操作
- 触屏滑动:支持移动端触摸操作
- 实时反馈:滑块跟随鼠标实时移动
- 验证结果:成功/失败即时提示
- 自动刷新:验证失败后自动刷新
5.2 使用指南
5.2.1 项目启动步骤
环境准备
- JDK 1.8+
- Maven 3.6+
- Redis服务器
创建图片资源
在src/main/resources/images/目录下准备背景图片:
src/main/resources/
└── images/
├── bg1.jpg
├── bg2.jpg
├── bg3.jpg
├── bg4.jpg
└── bg5.jpg
图片要求:
- 尺寸:建议320x160像素
- 格式:JPG/PNG
- 内容:风景、建筑等清晰图片
启动Redis
# Linux/Mac redis-server # Windows redis-server.exe
启动项目
# Maven方式 mvn spring-boot:run # 或打包后运行 mvn clean package java -jar target/slider-captcha-1.0.0.jar
访问验证码页面
打开浏览器访问:http://localhost:8080/captcha/index.html
5.2.2 API接口说明
1. 生成验证码接口
GET /captcha/api/captcha/generate
Response:
{
"code": 200,
"message": "success",
"data": {
"token": "abc123...",
"backgroundImage": "data:image/png;base64,iVBORw0KG...",
"sliderImage": "data:image/png;base64,iVBORw0KG...",
"width": 320,
"height": 160,
"sliderWidth": 60,
"sliderHeight": 60
}
}
2. 验证滑块接口
POST /captcha/api/captcha/verify
Content-Type: application/json
Request:
{
"token": "abc123...",
"moveX": 150,
"moveY": 0
}
Response:
{
"code": 200,
"message": "验证成功",
"data": "verify_token_xyz..."
}
3. 二次校验接口
POST /captcha/api/captcha/check?verifyToken=verify_token_xyz...
Response:
{
"code": 200,
"message": "校验通过",
"data": null
}
5.2.3 业务集成示例
在业务接口中集成验证码验证:
@PostMapping("/login")
public Result<LoginResponse> login(@RequestBody LoginRequest request) {
// 1. 验证验证码
boolean verified = captchaService.verifyToken(request.getVerifyToken());
if (!verified) {
return Result.error("验证码校验失败");
}
// 2. 执行登录逻辑
User user = userService.authenticate(request.getUsername(),
request.getPassword());
if (user == null) {
return Result.error("用户名或密码错误");
}
// 3. 生成登录令牌
String token = generateLoginToken(user);
return Result.success(new LoginResponse(token, user));
}
5.3 注意事项
安全性注意事项
- 验证码一次性使用
- 验证成功后立即删除Redis中的验证信息
- 防止同一验证码被多次使用
- 加密传输
- 滑块坐标等重要信息使用AES加密
- 防止客户端篡改验证数据
- 防止暴力 破解
- 设置合理的容错范围(5像素)
- 限制单个IP的验证频率
- 验证失败次数过多时锁定
- 分布式部署
- 使用Redis存储验证信息
- 多个应用实例共享验证状态
- 确保分布式环境下的一致性
性能优化建议
- 图片缓存
- 背景图片预加载到内存
- 减少图片IO操作
- Redis优化
- 使用连接池管理连接
- 设置合理的过期时间
- 定期清理过期数据
- 并发控制
- 限制单个用户同时生成的验证码数量
- 防止恶意请求占用系统资源
兼容性处理
- 移动端适配
- 支持Touch事件
- 调整滑块大小以适应手指操作
- 优化触摸响应速度
- 浏览器兼容
- 使用标准CSS属性
- 避免使用实验性API
- 提供降级方案
- 网络优化
- 图片压缩传输
- 使用CDN加速图片加载
- 提供离线降级方案
六、总结
本文详细介绍了Spring Boot整合动态图片滑动验证码的完整实现方案,从原理阐述、技术选型、代码实现到使用说明,涵盖了开发过程中的各个环节。
通过本方案,开发者可以快速在自己的项目中集成滑动验证码功能,提升系统的安全性和用户体验。该方案具有以下优势:
- 完整性强:从后端到前端,提供完整的代码实现
- 易于扩展:模块化设计,方便二次开发和功能扩展
- 安全可靠:采用多种安全机制,有效防止自动化攻击
- 性能优良:基于Redis缓存,支持高并发场景
希望本文能帮助开发者更好地理解和实现滑动验证码功能,在实际项目中发挥价值。
以上就是基于SpringBoot实现图片滑动验证码功能的详细内容,更多关于SpringBoot图片滑动验证码的资料请关注脚本之家其它相关文章!


最新评论