基于SpringBoot實現(xiàn)圖片滑動驗證碼功能
一、功能原理闡述
1.1 滑動驗證碼的工作原理
滑動驗證碼是一種基于行為驗證的人機識別機制,其核心思想是通過用戶的交互行為來區(qū)分真人操作與自動化程序。整個驗證流程包括以下幾個關(guān)鍵步驟:
圖像生成階段
- 系統(tǒng)從預(yù)設(shè)圖片庫中隨機選擇一張背景圖
- 在背景圖上隨機選擇一個位置,通過預(yù)定義的mask模板生成缺口形狀
- 將缺口位置的圖像區(qū)域裁剪出來作為滑塊拼圖
- 對背景圖的缺口位置進行半透明或模糊處理,形成明顯的視覺提示
用戶交互階段
- 前端展示帶有缺口的背景圖和對應(yīng)的滑塊拼圖
- 用戶通過鼠標拖拽或手指滑動,將滑塊移動到正確位置
- 系統(tǒng)實時記錄用戶的滑動軌跡、速度、加速度等行為數(shù)據(jù)
驗證判斷階段
- 用戶釋放滑塊后,前端收集最終的位移坐標和完整軌跡數(shù)據(jù)
- 將數(shù)據(jù)提交到后端進行驗證
- 后端對比滑塊位置與實際缺口位置的匹配度
- 結(jié)合行為特征(軌跡平滑度、滑動時間、加速度變化等)綜合判斷
1.2 安全價值分析
滑動驗證碼相比傳統(tǒng)驗證碼具有顯著的安全優(yōu)勢:
防爬蟲能力
- 傳統(tǒng)字符驗證碼容易被OCR技術(shù)自動識別
- 滑動驗證碼要求用戶進行復(fù)雜的空間定位操作,增加自動化難度
- 引入行為軌跡分析,機器人難以模擬真實用戶的操作特征
防暴力 破解
- 每次驗證都生成隨機缺口位置,防止預(yù)先破解
- 圖像庫多樣化,避免固定模式被機器學(xué)習(xí)模型識別
- 設(shè)置合理的容錯范圍,在用戶體驗和安全性之間取得平衡
防重放攻擊
- 驗證令牌具有時效性,過期即失效
- 結(jié)合用戶會話信息,防止驗證碼被跨會話使用
- 前端加密傳輸,防止中間人攻擊
1.3 應(yīng)用場景
滑動驗證碼適用于多種安全敏感的業(yè)務(wù)場景:
用戶認證場景
- 用戶登錄:防止惡意暴力 破解密碼
- 用戶注冊:攔截批量虛假賬號注冊
- 密碼找回:保護用戶賬號安全
業(yè)務(wù)操作場景
- 發(fā)表評論:防止惡意刷 評論、垃圾信息
- 內(nèi)容發(fā)布:防止自動化內(nèi)容提交
- 搶購活動:防止機器人搶購商品
- 表單提交:保護重要業(yè)務(wù)表單
數(shù)據(jù)保護場景
- 敏感信息查詢:防止惡意數(shù)據(jù)爬取
- 導(dǎo)出功能:防止批量數(shù)據(jù)導(dǎo)出
- API接口調(diào)用:保護接口不被濫用
二、技術(shù)選型說明
2.1 核心技術(shù)棧
基于Java生態(tài)體系,我們選擇以下技術(shù)棧實現(xiàn)滑動驗證碼功能:
| 技術(shù)組件 | 選型 | 選擇理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.7+ | 快速開發(fā),生態(tài)完善,配置簡化 |
| 圖像處理 | Java BufferedImage + 自定義算法 | 無需額外依賴,輕量高效 |
| 緩存存儲 | Redis | 分布式部署支持,高性能 |
| 前端技術(shù) | HTML5 + Canvas + JavaScript | 原生實現(xiàn),兼容性好,無框架依賴 |
| 加密算法 | AES-128 | 對稱加密,性能好,滿足安全需求 |
| 數(shù)據(jù)格式 | JSON | 標準格式,前后端交互友好 |
2.2 技術(shù)選型理由詳解
Java BufferedImage
- JDK自帶,無需引入第三方圖像處理庫
- 提供豐富的圖像操作API:裁剪、縮放、像素級操作
- 支持多種圖像格式:PNG、JPEG等
- 內(nèi)存占用合理,適合Web應(yīng)用場景
Redis緩存
- 滑動驗證碼需要存儲驗證狀態(tài)和臨時數(shù)據(jù)
- Redis提供高性能的key-value存儲
- 支持過期時間設(shè)置,自動清理過期驗證碼
- 分布式環(huán)境下保證多節(jié)點數(shù)據(jù)一致性
Canvas前端繪圖
- HTML5原生支持,無需插件
- 可以動態(tài)繪制背景圖和滑塊
- 實現(xiàn)拖拽交互響應(yīng)流暢
- 移動端和PC端統(tǒng)一實現(xiàn)
AES加密
- 滑動坐標等敏感信息需要加密傳輸
- AES算法成熟穩(wěn)定,性能優(yōu)異
- 防止客戶端篡改驗證數(shù)據(jù)
- Java提供標準加密庫支持
三、實現(xiàn)步驟詳解
3.1 項目環(huán)境搭建與依賴配置
創(chuàng)建Spring Boot項目
使用Spring Initializr創(chuàng)建新項目,選擇以下依賴:
- 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 核心算法實現(xiàn)
3.2.1 圖像處理工具類
創(chuàng)建圖片處理工具類,實現(xiàn)滑塊驗證碼的核心圖像處理功能:
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 驗證碼結(jié)果
*/
public static CaptchaResult generateCaptcha(BufferedImage originalImage,
int x, int y,
int width, int height) {
try {
// 1. 創(chuàng)建帶缺口的背景圖
BufferedImage backgroundImage = createBackgroundWithHole(
originalImage, x, y, width, height);
// 2. 創(chuàng)建滑塊圖片
BufferedImage sliderImage = createSliderImage(
originalImage, x, y, width, height);
// 3. 轉(zhuǎn)換為Base64
String backgroundBase64 = imageToBase64(backgroundImage);
String sliderBase64 = imageToBase64(sliderImage);
return new CaptchaResult(x, y, backgroundBase64, sliderBase64);
} catch (Exception e) {
throw new RuntimeException("生成驗證碼圖片失敗", e);
}
}
/**
* 創(chuàng)建帶缺口的背景圖
*/
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);
// 設(shè)置半透明填充
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
g2d.setColor(new Color(0, 0, 0, 100));
// 繪制缺口區(qū)域
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;
}
/**
* 創(chuàng)建滑塊圖片
*/
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);
// 裁剪滑塊區(qū)域
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) {
// 創(chuàng)建圓角形狀
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);
}
/**
* 圖片轉(zhuǎn)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);
}
/**
* 驗證碼結(jié)果類
*/
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 驗證碼服務(wù)類
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;
/**
* 驗證碼服務(wù)類
*/
@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. 返回結(jié)果
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;
}
}
/**
* 二次校驗(用于業(yè)務(wù)接口驗證)
*/
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 前后端交互設(shè)計
3.3.1 Controller層實現(xiàn)
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("驗證失敗");
}
}
/**
* 二次校驗接口(業(yè)務(wù)接口調(diào)用)
*/
@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;
/**
* 驗證碼響應(yīng)
*/
@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; // 行為軌跡(可選)
}
/**
* 通用結(jié)果
*/
@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 前端頁面實現(xiàn)
<!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('網(wǎng)絡(luò)錯誤', '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';
}
// 結(jié)束拖拽
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('網(wǎng)絡(luò)錯誤', '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';
}
// 顯示/隱藏加載狀態(tài)
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節(jié)提供,此處補充主要依賴版本信息:
<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 最終實現(xiàn)效果
完成上述實現(xiàn)后,滑動驗證碼功能將具備以下特性:
視覺效果
- 背景圖:320x160像素的高質(zhì)量圖片
- 滑塊圖:60x60像素的圓角矩形拼圖
- 缺口效果:半透明黑色遮罩,白色邊框高亮
- 滑動條:綠色漸變按鈕,拖動流暢
交互體驗
- 鼠標拖拽:支持PC端鼠標操作
- 觸屏滑動:支持移動端觸摸操作
- 實時反饋:滑塊跟隨鼠標實時移動
- 驗證結(jié)果:成功/失敗即時提示
- 自動刷新:驗證失敗后自動刷新
5.2 使用指南
5.2.1 項目啟動步驟
環(huán)境準備
- JDK 1.8+
- Maven 3.6+
- Redis服務(wù)器
創(chuàng)建圖片資源
在src/main/resources/images/目錄下準備背景圖片:
src/main/resources/
└── images/
├── bg1.jpg
├── bg2.jpg
├── bg3.jpg
├── bg4.jpg
└── bg5.jpg
圖片要求:
- 尺寸:建議320x160像素
- 格式:JPG/PNG
- 內(nèi)容:風(fēng)景、建筑等清晰圖片
啟動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 業(yè)務(wù)集成示例
在業(yè)務(wù)接口中集成驗證碼驗證:
@PostMapping("/login")
public Result<LoginResponse> login(@RequestBody LoginRequest request) {
// 1. 驗證驗證碼
boolean verified = captchaService.verifyToken(request.getVerifyToken());
if (!verified) {
return Result.error("驗證碼校驗失敗");
}
// 2. 執(zhí)行登錄邏輯
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加密
- 防止客戶端篡改驗證數(shù)據(jù)
- 防止暴力 破解
- 設(shè)置合理的容錯范圍(5像素)
- 限制單個IP的驗證頻率
- 驗證失敗次數(shù)過多時鎖定
- 分布式部署
- 使用Redis存儲驗證信息
- 多個應(yīng)用實例共享驗證狀態(tài)
- 確保分布式環(huán)境下的一致性
性能優(yōu)化建議
- 圖片緩存
- 背景圖片預(yù)加載到內(nèi)存
- 減少圖片IO操作
- Redis優(yōu)化
- 使用連接池管理連接
- 設(shè)置合理的過期時間
- 定期清理過期數(shù)據(jù)
- 并發(fā)控制
- 限制單個用戶同時生成的驗證碼數(shù)量
- 防止惡意請求占用系統(tǒng)資源
兼容性處理
- 移動端適配
- 支持Touch事件
- 調(diào)整滑塊大小以適應(yīng)手指操作
- 優(yōu)化觸摸響應(yīng)速度
- 瀏覽器兼容
- 使用標準CSS屬性
- 避免使用實驗性API
- 提供降級方案
- 網(wǎng)絡(luò)優(yōu)化
- 圖片壓縮傳輸
- 使用CDN加速圖片加載
- 提供離線降級方案
六、總結(jié)
本文詳細介紹了Spring Boot整合動態(tài)圖片滑動驗證碼的完整實現(xiàn)方案,從原理闡述、技術(shù)選型、代碼實現(xiàn)到使用說明,涵蓋了開發(fā)過程中的各個環(huán)節(jié)。
通過本方案,開發(fā)者可以快速在自己的項目中集成滑動驗證碼功能,提升系統(tǒng)的安全性和用戶體驗。該方案具有以下優(yōu)勢:
- 完整性強:從后端到前端,提供完整的代碼實現(xiàn)
- 易于擴展:模塊化設(shè)計,方便二次開發(fā)和功能擴展
- 安全可靠:采用多種安全機制,有效防止自動化攻擊
- 性能優(yōu)良:基于Redis緩存,支持高并發(fā)場景
希望本文能幫助開發(fā)者更好地理解和實現(xiàn)滑動驗證碼功能,在實際項目中發(fā)揮價值。
以上就是基于SpringBoot實現(xiàn)圖片滑動驗證碼功能的詳細內(nèi)容,更多關(guān)于SpringBoot圖片滑動驗證碼的資料請關(guān)注腳本之家其它相關(guān)文章!

