基于SpringBoot实现图片滑动验证码功能

 更新时间:2026年02月04日 08:56:46   作者:J_liaty  
本文详细讲解如何在Spring Boot项目中实现图片滑动验证码功能,从原理阐述到完整代码实现,帮助开发者快速掌握这一实用的安全验证技术,需要的朋友可以参考下

一、功能原理阐述

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 注意事项

安全性注意事项

  1. 验证码一次性使用
    • 验证成功后立即删除Redis中的验证信息
    • 防止同一验证码被多次使用
  2. 加密传输
    • 滑块坐标等重要信息使用AES加密
    • 防止客户端篡改验证数据
  3. 防止暴力 破解
    • 设置合理的容错范围(5像素)
    • 限制单个IP的验证频率
    • 验证失败次数过多时锁定
  4. 分布式部署
    • 使用Redis存储验证信息
    • 多个应用实例共享验证状态
    • 确保分布式环境下的一致性

性能优化建议

  • 图片缓存
    • 背景图片预加载到内存
    • 减少图片IO操作
  • Redis优化
    • 使用连接池管理连接
    • 设置合理的过期时间
    • 定期清理过期数据
  • 并发控制
    • 限制单个用户同时生成的验证码数量
    • 防止恶意请求占用系统资源

兼容性处理

  1. 移动端适配
    • 支持Touch事件
    • 调整滑块大小以适应手指操作
    • 优化触摸响应速度
  2. 浏览器兼容
    • 使用标准CSS属性
    • 避免使用实验性API
    • 提供降级方案
  3. 网络优化
    • 图片压缩传输
    • 使用CDN加速图片加载
    • 提供离线降级方案

六、总结

本文详细介绍了Spring Boot整合动态图片滑动验证码的完整实现方案,从原理阐述、技术选型、代码实现到使用说明,涵盖了开发过程中的各个环节。

通过本方案,开发者可以快速在自己的项目中集成滑动验证码功能,提升系统的安全性和用户体验。该方案具有以下优势:

  • 完整性强:从后端到前端,提供完整的代码实现
  • 易于扩展:模块化设计,方便二次开发和功能扩展
  • 安全可靠:采用多种安全机制,有效防止自动化攻击
  • 性能优良:基于Redis缓存,支持高并发场景

希望本文能帮助开发者更好地理解和实现滑动验证码功能,在实际项目中发挥价值。

以上就是基于SpringBoot实现图片滑动验证码功能的详细内容,更多关于SpringBoot图片滑动验证码的资料请关注脚本之家其它相关文章!

相关文章

  • Scala入门教程详解

    Scala入门教程详解

    这篇文章主要介绍了Scala入门教程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-06-06
  • MyBatis-plus批量插入的通用方法使用

    MyBatis-plus批量插入的通用方法使用

    mybatis-plus的IService接口默认提供saveBatch批量插入,也是唯一一个默认批量插入,在数据量不是很大的情况下可以直接使用,本文带你详细了解MyBatis-plus 批量插入的通用方法及使用方法,需要的朋友可以参考一下
    2023-04-04
  • Mac安装Maven的几种方法小结

    Mac安装Maven的几种方法小结

    本文主要介绍了Mac安装Maven的几种方法小结,主要包括通过Homebrew安装Maven,通过SDKMAN安装Maven和通过官方网站下载安装包安装Maven,感兴趣的可以了解一下
    2024-01-01
  • 关于Java中的可见性和有序性问题

    关于Java中的可见性和有序性问题

    这篇文章主要介绍了关于Java中的可见性和有序性问题,Java在诞生之初就支持多线程,自然也有针对这三者的技术方案,今天就学习一下Java如何解决其中的可见性和有序性导致的问题,需要的朋友可以参考下
    2023-08-08
  • Springmvc异常处理器及拦截器实现代码

    Springmvc异常处理器及拦截器实现代码

    这篇文章主要介绍了Springmvc异常处理器及拦截器实现代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • Java多线程及分布式爬虫架构原理解析

    Java多线程及分布式爬虫架构原理解析

    这篇文章主要介绍了Java多线程及分布式爬虫架构原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-10-10
  • 简单了解Java创建线程两种方法

    简单了解Java创建线程两种方法

    这篇文章主要介绍了简单了解Java创建线程两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • 浅谈Java编程ToString()方法重写的意义

    浅谈Java编程ToString()方法重写的意义

    这篇文章主要介绍了浅谈Java编程ToString()方法重写的意义,还是挺不错的,这里分享给大家,供朋友们学习和参考。
    2017-10-10
  • 使用JPA自定义SQL查询结果

    使用JPA自定义SQL查询结果

    这篇文章主要介绍了使用JPA自定义SQL查询结果,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Mybatis中单双引号引发的惨案及解决

    Mybatis中单双引号引发的惨案及解决

    这篇文章主要介绍了Mybatis中单双引号引发的惨案及解决方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01

最新评论