最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于SpringBoot實(shí)現(xiàn)一個(gè)安全可靠的滑塊拼圖驗(yàn)證系統(tǒng)

 更新時(shí)間:2026年03月23日 08:30:34   作者:悟空碼字  
滑塊拼圖驗(yàn)證是一種行為驗(yàn)證技術(shù),通過要求用戶將拼圖塊拖動(dòng)到正確位置來區(qū)分人類用戶和自動(dòng)化程序,本文給大家介紹了基于SpringBoot實(shí)現(xiàn)滑塊拼圖驗(yàn)證的完整代碼,需要的朋友可以參考下

一、滑塊拼圖驗(yàn)證碼概述

1.1 什么是滑塊拼圖驗(yàn)證

滑塊拼圖驗(yàn)證是一種行為驗(yàn)證技術(shù),通過要求用戶將拼圖塊拖動(dòng)到正確位置來區(qū)分人類用戶和自動(dòng)化程序。它由以下幾個(gè)核心部分組成:

  • 背景圖片:完整的原始圖片
  • 滑塊圖片:從背景圖片中切割出的小塊拼圖
  • 缺口位置:背景圖片上被切割后留下的凹槽
  • 滑塊軌道:用戶拖動(dòng)滑塊的滑動(dòng)條

1.2 工作原理

  1. 服務(wù)端生成驗(yàn)證碼時(shí),在背景圖片上隨機(jī)位置切割出一個(gè)滑塊圖片
  2. 記錄下這個(gè)缺口位置的X坐標(biāo)(目標(biāo)位置)
  3. 前端顯示背景圖(帶缺口)、滑塊圖和滑塊軌道
  4. 用戶需要將滑塊拖動(dòng)到缺口位置
  5. 前端將用戶拖動(dòng)距離上傳到服務(wù)端
  6. 服務(wù)端驗(yàn)證拖動(dòng)距離與目標(biāo)位置的偏差是否在允許范圍內(nèi)

1.3 安全特性

  • 軌跡驗(yàn)證:分析用戶的拖動(dòng)軌跡,判斷是否為真人操作
  • 時(shí)間驗(yàn)證:驗(yàn)證拖動(dòng)用時(shí)是否合理
  • 隨機(jī)性:每次驗(yàn)證的缺口位置都是隨機(jī)的
  • 加密傳輸:關(guān)鍵參數(shù)加密處理,防止篡改

1.4 優(yōu)勢特點(diǎn)

  • 用戶體驗(yàn)好:操作直觀簡單
  • 安全性高:難以被自動(dòng)化程序 破解
  • 無干擾:不需要識(shí)別扭曲的文字或圖片
  • 適應(yīng)性強(qiáng):支持移動(dòng)端和PC端

二、SpringBoot實(shí)現(xiàn)詳細(xì)步驟

2.1 環(huán)境準(zhǔn)備

pom.xml依賴配置

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Redis for session management -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- 圖片處理 -->
    <dependency>
        <groupId>javax.imageio</groupId>
        <artifactId>imageio-core</artifactId>
        <version>1.0</version>
    </dependency>
    <!-- Base64編碼 -->
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
    </dependency>
    <!-- 工具類 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
</dependencies>

application.yml配置

server:
  port: 8080
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 10000ms
# 滑塊驗(yàn)證配置
slider:
  # 圖片寬度
  image-width: 320
  # 圖片高度
  image-height: 160
  # 滑塊寬度
  slider-width: 50
  # 滑塊高度
  slider-height: 50
  # 允許的誤差范圍
  tolerance: 5
  # 過期時(shí)間(秒)
  expire-time: 120

2.2 核心類設(shè)計(jì)

滑塊驗(yàn)證碼實(shí)體類

@Data
public class SliderCaptcha {
    /** 唯一標(biāo)識(shí) */
    private String id;
    
    /** 背景圖片Base64 */
    private String backgroundImage;
    
    /** 滑塊圖片Base64 */
    private String sliderImage;
    
    /** 目標(biāo)位置X坐標(biāo) */
    private Integer targetX;
    
    /** 目標(biāo)位置Y坐標(biāo) */
    private Integer targetY;
    
    /** 滑塊寬度 */
    private Integer sliderWidth;
    
    /** 滑塊高度 */
    private Integer sliderHeight;
    
    /** 背景寬度 */
    private Integer bgWidth;
    
    /** 背景高度 */
    private Integer bgHeight;
}

驗(yàn)證請(qǐng)求DTO

@Data
public class VerifyRequest {
    /** 驗(yàn)證碼ID */
    private String captchaId;
    
    /** 拖動(dòng)距離X */
    private Integer moveX;
    
    /** 拖動(dòng)距離Y */
    private Integer moveY;
    
    /** 拖動(dòng)軌跡 */
    private String track;
    
    /** 拖動(dòng)用時(shí)(毫秒) */
    private Long duration;
    
    /** 加密參數(shù) */
    private String token;
}

驗(yàn)證結(jié)果DTO

@Data
@AllArgsConstructor
public class VerifyResult {
    /** 是否成功 */
    private Boolean success;
    
    /** 消息 */
    private String message;
    
    /** 驗(yàn)證令牌(成功時(shí)返回) */
    private String token;
}

2.3 圖片處理工具類

@Component
public class ImageUtil {
    
    /**
     * 從資源目錄隨機(jī)獲取一張圖片
     */
    public BufferedImage getRandomImage() {
        try {
            // 從resources/images目錄獲取隨機(jī)圖片
            ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            Resource[] resources = resolver.getResources("classpath:images/*.jpg");
            
            if (resources.length == 0) {
                // 如果沒有圖片,創(chuàng)建默認(rèn)圖片
                return createDefaultImage();
            }
            
            Random random = new Random();
            Resource resource = resources[random.nextInt(resources.length)];
            return ImageIO.read(resource.getInputStream());
            
        } catch (IOException e) {
            log.error("讀取圖片失敗", e);
            return createDefaultImage();
        }
    }
    
    /**
     * 創(chuàng)建默認(rèn)圖片(當(dāng)沒有資源圖片時(shí)使用)
     */
    private BufferedImage createDefaultImage() {
        BufferedImage image = new BufferedImage(320, 160, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();
        
        // 創(chuàng)建漸變背景
        GradientPaint gradient = new GradientPaint(
            0, 0, Color.LIGHT_GRAY, 
            320, 160, Color.DARK_GRAY
        );
        g.setPaint(gradient);
        g.fillRect(0, 0, 320, 160);
        
        // 添加一些噪點(diǎn)防止簡單破解
        g.setColor(Color.WHITE);
        Random random = new Random();
        for (int i = 0; i < 100; i++) {
            int x = random.nextInt(320);
            int y = random.nextInt(160);
            g.fillRect(x, y, 1, 1);
        }
        
        g.dispose();
        return image;
    }
    
    /**
     * 圖片轉(zhuǎn)Base64
     */
    public String imageToBase64(BufferedImage image) {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            ImageIO.write(image, "png", baos);
            byte[] bytes = baos.toByteArray();
            return "data:image/png;base64," + Base64.getEncoder().encodeToString(bytes);
        } catch (IOException e) {
            log.error("圖片轉(zhuǎn)Base64失敗", e);
            return null;
        }
    }
    
    /**
     * 生成滑塊拼圖
     * @param bgImage 背景圖片
     * @param targetX 目標(biāo)位置X
     * @param targetY 目標(biāo)位置Y
     * @param sliderWidth 滑塊寬度
     * @param sliderHeight 滑塊高度
     * @return 包含滑塊圖片和背景圖片的對(duì)象
     */
    public SliderImageInfo generateSliderImage(BufferedImage bgImage, 
                                               int targetX, 
                                               int targetY,
                                               int sliderWidth,
                                               int sliderHeight) {
        
        int bgWidth = bgImage.getWidth();
        int bgHeight = bgImage.getHeight();
        
        // 創(chuàng)建滑塊圖片
        BufferedImage sliderImage = new BufferedImage(sliderWidth, sliderHeight, BufferedImage.TYPE_4BYTE_ABGR);
        Graphics2D sliderGraphics = sliderImage.createGraphics();
        
        // 復(fù)制背景圖上的滑塊區(qū)域到滑塊圖片
        sliderGraphics.drawImage(bgImage, 
                                0, 0, sliderWidth, sliderHeight,
                                targetX, targetY, targetX + sliderWidth, targetY + sliderHeight,
                                null);
        
        // 對(duì)滑塊邊緣進(jìn)行處理,制造凹凸效果
        processSliderEdges(sliderImage, sliderGraphics);
        
        // 在背景圖上制造缺口(填充白色并添加陰影)
        Graphics2D bgGraphics = bgImage.createGraphics();
        createHole(bgGraphics, targetX, targetY, sliderWidth, sliderHeight);
        
        sliderGraphics.dispose();
        bgGraphics.dispose();
        
        return new SliderImageInfo(bgImage, sliderImage);
    }
    
    /**
     * 處理滑塊邊緣,制造凹凸效果
     */
    private void processSliderEdges(BufferedImage sliderImage, Graphics2D graphics) {
        int width = sliderImage.getWidth();
        int height = sliderImage.getHeight();
        
        // 添加陰影效果
        graphics.setColor(new Color(0, 0, 0, 80));
        graphics.setStroke(new BasicStroke(2f));
        graphics.drawRect(0, 0, width - 1, height - 1);
        
        // 添加內(nèi)陰影
        graphics.setColor(new Color(255, 255, 255, 50));
        graphics.drawRect(1, 1, width - 3, height - 3);
        
        // 邊緣模糊處理
        float[] kernel = new float[9];
        Arrays.fill(kernel, 1f/9f);
        ConvolveOp blur = new ConvolveOp(new Kernel(3, 3, kernel));
        sliderImage.setRGB(0, 0, width, height, 
                          blur.filter(sliderImage.getRGB(0, 0, width, height, null, 0, width), 
                          null, 0, width), 0, width);
    }
    
    /**
     * 在背景圖上創(chuàng)建缺口
     */
    private void createHole(Graphics2D graphics, int x, int y, int width, int height) {
        // 填充白色表示缺口
        graphics.setColor(Color.WHITE);
        graphics.fillRect(x, y, width, height);
        
        // 添加缺口陰影
        graphics.setColor(new Color(0, 0, 0, 50));
        graphics.setStroke(new BasicStroke(2f));
        graphics.drawRect(x, y, width - 1, height - 1);
        
        // 添加缺口內(nèi)陰影
        graphics.setColor(new Color(255, 255, 255, 100));
        graphics.drawRect(x + 1, y + 1, width - 3, height - 3);
    }
    
    /**
     * 隨機(jī)生成缺口位置
     */
    public Point generateRandomPosition(int bgWidth, int bgHeight, 
                                        int sliderWidth, int sliderHeight) {
        Random random = new Random();
        
        // 確?;瑝K在圖片范圍內(nèi),留出邊緣空間
        int minX = 20;
        int maxX = bgWidth - sliderWidth - 20;
        int minY = 10;
        int maxY = bgHeight - sliderHeight - 10;
        
        int targetX = random.nextInt(maxX - minX + 1) + minX;
        int targetY = random.nextInt(maxY - minY + 1) + minY;
        
        return new Point(targetX, targetY);
    }
    
    @Data
    @AllArgsConstructor
    public static class SliderImageInfo {
        private BufferedImage background;
        private BufferedImage slider;
    }
}

2.4 核心服務(wù)類

@Service
@Slf4j
public class SliderCaptchaService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private ImageUtil imageUtil;
    
    @Value("${slider.image-width:320}")
    private int imageWidth;
    
    @Value("${slider.image-height:160}")
    private int imageHeight;
    
    @Value("${slider.slider-width:50}")
    private int sliderWidth;
    
    @Value("${slider.slider-height:50}")
    private int sliderHeight;
    
    @Value("${slider.tolerance:5}")
    private int tolerance;
    
    @Value("${slider.expire-time:120}")
    private long expireTime;
    
    /**
     * 生成滑塊驗(yàn)證碼
     */
    public SliderCaptcha generate() {
        try {
            // 1. 獲取隨機(jī)背景圖片
            BufferedImage originalImage = imageUtil.getRandomImage();
            
            // 2. 調(diào)整圖片大小
            originalImage = resizeImage(originalImage, imageWidth, imageHeight);
            
            // 3. 隨機(jī)生成缺口位置
            Point targetPoint = imageUtil.generateRandomPosition(
                imageWidth, imageHeight, sliderWidth, sliderHeight
            );
            
            // 4. 生成滑塊圖片和帶缺口的背景圖片
            ImageUtil.SliderImageInfo sliderImageInfo = imageUtil.generateSliderImage(
                originalImage, 
                targetPoint.x, 
                targetPoint.y,
                sliderWidth, 
                sliderHeight
            );
            
            // 5. 圖片轉(zhuǎn)Base64
            String backgroundBase64 = imageUtil.imageToBase64(sliderImageInfo.getBackground());
            String sliderBase64 = imageUtil.imageToBase64(sliderImageInfo.getSlider());
            
            // 6. 創(chuàng)建驗(yàn)證碼對(duì)象
            SliderCaptcha captcha = new SliderCaptcha();
            String captchaId = UUID.randomUUID().toString();
            captcha.setId(captchaId);
            captcha.setBackgroundImage(backgroundBase64);
            captcha.setSliderImage(sliderBase64);
            captcha.setTargetX(targetPoint.x);
            captcha.setTargetY(targetPoint.y);
            captcha.setSliderWidth(sliderWidth);
            captcha.setSliderHeight(sliderHeight);
            captcha.setBgWidth(imageWidth);
            captcha.setBgHeight(imageHeight);
            
            // 7. 將目標(biāo)位置存入Redis
            saveToRedis(captchaId, captcha);
            
            return captcha;
            
        } catch (Exception e) {
            log.error("生成滑塊驗(yàn)證碼失敗", e);
            throw new RuntimeException("生成驗(yàn)證碼失敗", e);
        }
    }
    
    /**
     * 調(diào)整圖片大小
     */
    private BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) {
        BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = resizedImage.createGraphics();
        g.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
        g.dispose();
        return resizedImage;
    }
    
    /**
     * 保存驗(yàn)證碼信息到Redis
     */
    private void saveToRedis(String captchaId, SliderCaptcha captcha) {
        // 保存目標(biāo)位置
        Map<String, String> map = new HashMap<>();
        map.put("targetX", String.valueOf(captcha.getTargetX()));
        map.put("targetY", String.valueOf(captcha.getTargetY()));
        map.put("timestamp", String.valueOf(System.currentTimeMillis()));
        
        redisTemplate.opsForHash().putAll(getRedisKey(captchaId), map);
        redisTemplate.expire(getRedisKey(captchaId), expireTime, TimeUnit.SECONDS);
    }
    
    /**
     * 驗(yàn)證滑塊拖動(dòng)
     */
    public VerifyResult verify(VerifyRequest request) {
        try {
            // 1. 基本參數(shù)驗(yàn)證
            if (request == null || request.getCaptchaId() == null) {
                return new VerifyResult(false, "參數(shù)錯(cuò)誤", null);
            }
            
            // 2. 從Redis獲取目標(biāo)位置
            Map<Object, Object> captchaData = redisTemplate.opsForHash()
                .entries(getRedisKey(request.getCaptchaId()));
            
            if (captchaData.isEmpty()) {
                return new VerifyResult(false, "驗(yàn)證碼已過期", null);
            }
            
            int targetX = Integer.parseInt(captchaData.get("targetX").toString());
            int targetY = Integer.parseInt(captchaData.get("targetY").toString());
            long generateTime = Long.parseLong(captchaData.get("timestamp").toString());
            
            // 3. 驗(yàn)證軌跡
            if (!validateTrack(request, generateTime)) {
                return new VerifyResult(false, "軌跡驗(yàn)證失敗", null);
            }
            
            // 4. 驗(yàn)證拖動(dòng)距離
            int deviation = Math.abs(request.getMoveX() - targetX);
            
            if (deviation <= tolerance) {
                // 驗(yàn)證成功,刪除已使用的驗(yàn)證碼
                redisTemplate.delete(getRedisKey(request.getCaptchaId()));
                
                // 生成驗(yàn)證令牌
                String token = generateToken(request.getCaptchaId());
                
                return new VerifyResult(true, "驗(yàn)證成功", token);
            } else {
                return new VerifyResult(false, "驗(yàn)證失敗,請(qǐng)重試", null);
            }
            
        } catch (Exception e) {
            log.error("驗(yàn)證失敗", e);
            return new VerifyResult(false, "驗(yàn)證異常", null);
        }
    }
    
    /**
     * 軌跡驗(yàn)證
     */
    private boolean validateTrack(VerifyRequest request, long generateTime) {
        // 1. 驗(yàn)證時(shí)間是否合理
        long currentTime = System.currentTimeMillis();
        long timeDiff = currentTime - generateTime;
        
        if (timeDiff > expireTime * 1000) {
            return false; // 超時(shí)
        }
        
        // 2. 驗(yàn)證拖動(dòng)時(shí)間(通常在300ms到3000ms之間)
        if (request.getDuration() == null || 
            request.getDuration() < 300 || 
            request.getDuration() > 30000) {
            return false;
        }
        
        // 3. 驗(yàn)證軌跡數(shù)據(jù)(如果有)
        if (request.getTrack() != null) {
            // 這里可以解析軌跡JSON,驗(yàn)證加速度、抖動(dòng)等特征
            return validateTrackData(request.getTrack());
        }
        
        return true;
    }
    
    /**
     * 驗(yàn)證軌跡數(shù)據(jù)
     */
    private boolean validateTrackData(String track) {
        try {
            // 解析軌跡JSON
            // 示例:[{"x":0,"y":0,"t":0},{"x":10,"y":0,"t":100},...]
            List<TrackPoint> points = parseTrack(track);
            
            if (points.isEmpty()) {
                return false;
            }
            
            // 驗(yàn)證軌跡合理性
            boolean hasMovement = false;
            int lastX = -1;
            int continuousFrames = 0;
            
            for (TrackPoint point : points) {
                // 檢查是否有移動(dòng)
                if (point.getX() > 0) {
                    hasMovement = true;
                }
                
                // 檢查是否連續(xù)移動(dòng)(防止簡單模擬)
                if (lastX >= 0 && Math.abs(point.getX() - lastX) <= 1) {
                    continuousFrames++;
                } else {
                    continuousFrames = 0;
                }
                
                // 如果連續(xù)3幀沒有移動(dòng),可能有問題
                if (continuousFrames > 3) {
                    return false;
                }
                
                lastX = point.getX();
            }
            
            return hasMovement;
            
        } catch (Exception e) {
            log.warn("軌跡數(shù)據(jù)解析失敗", e);
            return false;
        }
    }
    
    /**
     * 解析軌跡JSON
     */
    private List<TrackPoint> parseTrack(String trackJson) {
        // 這里可以使用Jackson解析JSON
        // 簡化實(shí)現(xiàn),實(shí)際使用時(shí)需要正確解析
        return new ArrayList<>();
    }
    
    /**
     * 生成驗(yàn)證令牌
     */
    private String generateToken(String captchaId) {
        String token = UUID.randomUUID().toString();
        // 存儲(chǔ)令牌,有效期可設(shè)置短一些
        redisTemplate.opsForValue().set(
            "token:" + token, 
            captchaId, 
            5, 
            TimeUnit.MINUTES
        );
        return token;
    }
    
    /**
     * 獲取Redis鍵名
     */
    private String getRedisKey(String captchaId) {
        return "captcha:slider:" + captchaId;
    }
    
    /**
     * 軌跡點(diǎn)內(nèi)部類
     */
    @Data
    private static class TrackPoint {
        private int x;
        private int y;
        private long t;
    }
}

2.5 控制器層

@RestController
@RequestMapping("/api/captcha")
@Slf4j
public class CaptchaController {
    
    @Autowired
    private SliderCaptchaService captchaService;
    
    /**
     * 獲取滑塊驗(yàn)證碼
     */
    @GetMapping("/slider/generate")
    public ResponseEntity<SliderCaptcha> generate() {
        try {
            SliderCaptcha captcha = captchaService.generate();
            return ResponseEntity.ok(captcha);
        } catch (Exception e) {
            log.error("生成驗(yàn)證碼失敗", e);
            return ResponseEntity.status(500).body(null);
        }
    }
    
    /**
     * 驗(yàn)證滑塊
     */
    @PostMapping("/slider/verify")
    public ResponseEntity<VerifyResult> verify(@RequestBody VerifyRequest request) {
        try {
            VerifyResult result = captchaService.verify(request);
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            log.error("驗(yàn)證失敗", e);
            return ResponseEntity.status(500)
                .body(new VerifyResult(false, "驗(yàn)證服務(wù)異常", null));
        }
    }
    
    /**
     * 檢查驗(yàn)證狀態(tài)
     */
    @GetMapping("/slider/check/{token}")
    public ResponseEntity<Boolean> checkStatus(@PathVariable String token) {
        // 可以檢查token是否有效
        return ResponseEntity.ok(true);
    }
}

2.6 前端實(shí)現(xiàn)示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>滑塊拼圖驗(yàn)證</title>
    <style>
        .captcha-container {
            width: 340px;
            margin: 50px auto;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .bg-image-container {
            position: relative;
            width: 320px;
            height: 160px;
            margin-bottom: 20px;
            overflow: hidden;
        }
        .bg-image {
            width: 100%;
            height: 100%;
        }
        .slider-image {
            position: absolute;
            cursor: move;
            z-index: 10;
            width: 50px;
            height: 50px;
        }
        .slider-track {
            width: 320px;
            height: 40px;
            background: #f0f0f0;
            border-radius: 20px;
            position: relative;
            margin-top: 20px;
        }
        .slider-button {
            position: absolute;
            width: 40px;
            height: 40px;
            background: #4CAF50;
            border-radius: 50%;
            cursor: grab;
            left: 0;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        }
        .slider-button.dragging {
            cursor: grabbing;
            transform: scale(1.1);
        }
        .result-message {
            margin-top: 20px;
            padding: 10px;
            border-radius: 4px;
            text-align: center;
        }
        .success {
            background: #d4edda;
            color: #155724;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
        }
        .refresh-btn {
            background: #007bff;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            margin-top: 10px;
        }
    </style>
</head>
<body>
    <div class="captcha-container">
        <h3 style="text-align:center;">滑塊拼圖驗(yàn)證</h3>
        <!-- 驗(yàn)證碼區(qū)域 -->
        <div class="bg-image-container" id="captchaArea">
            <img class="bg-image" id="bgImage" src="" alt="背景圖">
            <img class="slider-image" id="sliderImage" src="" alt="滑塊" draggable="false">
        </div>
        <!-- 滑塊軌道 -->
        <div class="slider-track" id="sliderTrack">
            <div class="slider-button" id="sliderButton"></div>
        </div>
        <!-- 結(jié)果顯示 -->
        <div class="result-message" id="resultMessage"></div>
        <!-- 刷新按鈕 -->
        <button class="refresh-btn" onclick="refreshCaptcha()">刷新驗(yàn)證碼</button>
    </div>
    <script>
        // 當(dāng)前驗(yàn)證碼ID
        let captchaId = '';
        let targetX = 0;
        let isDragging = false;
        let startX = 0;
        let currentX = 0;
        let trackData = [];
        let startTime = 0;
        // 獲取驗(yàn)證碼
        async function getCaptcha() {
            try {
                const response = await fetch('/api/captcha/slider/generate');
                const data = await response.json();
                // 更新頁面
                document.getElementById('bgImage').src = data.backgroundImage;
                document.getElementById('sliderImage').src = data.sliderImage;
                // 保存驗(yàn)證碼信息
                captchaId = data.id;
                targetX = data.targetX;
                // 重置滑塊位置
                resetSlider();
            } catch (error) {
                console.error('獲取驗(yàn)證碼失敗:', error);
            }
        }
        // 重置滑塊
        function resetSlider() {
            const sliderButton = document.getElementById('sliderButton');
            const sliderImage = document.getElementById('sliderImage');
            sliderButton.style.left = '0px';
            sliderImage.style.left = '0px';
            document.getElementById('resultMessage').innerHTML = '';
        }
        // 刷新驗(yàn)證碼
        function refreshCaptcha() {
            getCaptcha();
        }
        // 初始化拖動(dòng)事件
        function initDragEvents() {
            const sliderButton = document.getElementById('sliderButton');
            const sliderImage = document.getElementById('sliderImage');
            const track = document.getElementById('sliderTrack');
            // 鼠標(biāo)按下
            sliderButton.addEventListener('mousedown', startDrag);
            // 觸摸事件支持
            sliderButton.addEventListener('touchstart', startDrag);
            // 全局鼠標(biāo)移動(dòng)和松開
            document.addEventListener('mousemove', drag);
            document.addEventListener('mouseup', stopDrag);
            document.addEventListener('touchmove', drag);
            document.addEventListener('touchend', stopDrag);
            function startDrag(e) {
                e.preventDefault();
                isDragging = true;
                sliderButton.classList.add('dragging');
                // 記錄起始位置和時(shí)間
                const clientX = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX;
                startX = clientX - sliderButton.offsetLeft;
                // 開始記錄軌跡
                trackData = [];
                startTime = Date.now();
                // 添加起始點(diǎn)
                trackData.push({
                    x: 0,
                    y: 0,
                    t: 0
                });
            }
            function drag(e) {
                if (!isDragging) return;
                e.preventDefault();
                const clientX = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX;
                // 計(jì)算新位置
                let newLeft = clientX - startX;
                // 限制范圍
                const maxLeft = track.offsetWidth - sliderButton.offsetWidth;
                newLeft = Math.max(0, Math.min(newLeft, maxLeft));
                // 更新滑塊位置
                sliderButton.style.left = newLeft + 'px';
                sliderImage.style.left = newLeft + 'px';
                // 記錄軌跡點(diǎn)(每10ms記錄一次)
                const currentTime = Date.now();
                if (trackData.length === 0 || currentTime - trackData[trackData.length - 1].t > 10) {
                    trackData.push({
                        x: newLeft,
                        y: 0,
                        t: currentTime - startTime
                    });
                }
                currentX = newLeft;
            }
            function stopDrag(e) {
                if (!isDragging) return;
                isDragging = false;
                sliderButton.classList.remove('dragging');
                // 記錄結(jié)束點(diǎn)
                trackData.push({
                    x: currentX,
                    y: 0,
                    t: Date.now() - startTime
                });
                // 驗(yàn)證滑塊
                verifySlider();
            }
        }
        // 驗(yàn)證滑塊
        async function verifySlider() {
            const duration = Date.now() - startTime;
            const request = {
                captchaId: captchaId,
                moveX: currentX,
                moveY: 0,
                track: JSON.stringify(trackData),
                duration: duration
            };
            try {
                const response = await fetch('/api/captcha/slider/verify', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(request)
                });
                const result = await response.json();
                const resultDiv = document.getElementById('resultMessage');
                if (result.success) {
                    resultDiv.className = 'result-message success';
                    resultDiv.innerHTML = '? 驗(yàn)證成功!';
                    // 驗(yàn)證成功后可以執(zhí)行后續(xù)操作
                    setTimeout(() => {
                        alert('驗(yàn)證通過,可以執(zhí)行下一步操作');
                    }, 500);
                } else {
                    resultDiv.className = 'result-message error';
                    resultDiv.innerHTML = '? ' + result.message;
                    // 驗(yàn)證失敗,重置滑塊
                    setTimeout(() => {
                        resetSlider();
                    }, 1000);
                }
            } catch (error) {
                console.error('驗(yàn)證失敗:', error);
            }
        }
        // 頁面加載時(shí)初始化
        window.onload = function() {
            getCaptcha();
            initDragEvents();
        };
    </script>
</body>
</html>

三、詳細(xì)總結(jié)

3.1 實(shí)現(xiàn)要點(diǎn)總結(jié)

  1. 圖片處理核心
    • 使用Java的BufferedImage進(jìn)行圖片操作
    • 通過Graphics2D實(shí)現(xiàn)圖片裁剪和特效處理
    • 使用Base64編碼傳輸圖片數(shù)據(jù)
  2. 安全驗(yàn)證機(jī)制
    • 目標(biāo)位置存儲(chǔ)在服務(wù)端(Redis)
    • 驗(yàn)證拖動(dòng)距離誤差在允許范圍內(nèi)
    • 軌跡數(shù)據(jù)分析防止機(jī)器模擬
    • 時(shí)間驗(yàn)證防止重放攻擊
    • 單次有效防止重復(fù)使用
  3. 性能優(yōu)化考慮
    • Redis存儲(chǔ)驗(yàn)證數(shù)據(jù),支持分布式部署
    • 圖片預(yù)加載減少實(shí)時(shí)處理開銷
    • 合理的過期時(shí)間避免內(nèi)存溢出
  4. 用戶體驗(yàn)設(shè)計(jì)
    • 滑塊邊緣特效增加真實(shí)感
    • 拖動(dòng)時(shí)反饋效果
    • 驗(yàn)證結(jié)果即時(shí)提示
    • 刷新功能方便重新嘗試

3.2 關(guān)鍵技術(shù)難點(diǎn)

  1. 圖片切割精確性
    • 需要確?;瑝K和缺口完全對(duì)應(yīng)
    • 邊緣處理要自然,避免明顯痕跡
  2. 軌跡驗(yàn)證算法
    • 需要區(qū)分真人和機(jī)器的拖動(dòng)特征
    • 機(jī)器學(xué)習(xí)可以進(jìn)一步提高準(zhǔn)確率
  3. 并發(fā)處理
    • 高并發(fā)下保證驗(yàn)證碼唯一性
    • 防止暴力 破解

3.3 改進(jìn)方向

安全性增強(qiáng)

// 添加加密參數(shù)
@Data
public class EncryptedRequest {
    private String data; // 加密的請(qǐng)求數(shù)據(jù)
    private String sign; // 簽名
}

// 使用AES加密關(guān)鍵參數(shù)
public String encryptData(String data, String key) {
    // AES加密實(shí)現(xiàn)
}

驗(yàn)證算法優(yōu)化

// 引入機(jī)器學(xué)習(xí)模型驗(yàn)證軌跡
public boolean mlValidateTrack(List<TrackPoint> track) {
    // 調(diào)用訓(xùn)練好的模型
    // 返回是否為真人操作
}

用戶體驗(yàn)優(yōu)化

  • 添加滑塊吸附效果
  • 錯(cuò)誤提示動(dòng)畫
  • 多種圖片主題切換

分布式支持

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {
        // 配置Redis序列化
        // 支持分布式部署
    }
}

3.4 注意事項(xiàng)

  1. 圖片資源管理
    • 定期更新圖片庫,防止被機(jī)器學(xué)習(xí)識(shí)別
    • 圖片大小控制,避免網(wǎng)絡(luò)傳輸過慢
  2. 異常處理
    • 驗(yàn)證碼生成失敗要有備用方案
    • 網(wǎng)絡(luò)超時(shí)處理
  3. 監(jiān)控告警
    • 記錄驗(yàn)證成功率
    • 監(jiān)控惡意請(qǐng)求
    • 設(shè)置閾值告警
  4. 合規(guī)性
    • 符合《個(gè)人信息保護(hù)法》
    • 驗(yàn)證數(shù)據(jù)定期清理
    • 不收集不必要的用戶信息

3.5 實(shí)際應(yīng)用場景

  1. 登錄注冊(cè)保護(hù)
    • 防止暴力 破解賬號(hào)
    • 阻止自動(dòng)化注冊(cè)
  2. 敏感操作確認(rèn)
    • 支付確認(rèn)
    • 修改重要信息
  3. 反爬蟲機(jī)制
    • 保護(hù)API接口
    • 防止數(shù)據(jù)抓取
  4. 活動(dòng)防刷
    • 限制重復(fù)參與
    • 防止薅羊毛行為

通過以上完整實(shí)現(xiàn),構(gòu)建了一個(gè)安全可靠的滑塊拼圖驗(yàn)證系統(tǒng)。該系統(tǒng)不僅實(shí)現(xiàn)了基本的滑塊驗(yàn)證功能,還加入了軌跡驗(yàn)證、時(shí)間驗(yàn)證等多重安全機(jī)制,能夠有效區(qū)分人類用戶和自動(dòng)化程序。

以上就是基于SpringBoot實(shí)現(xiàn)一個(gè)安全可靠的滑塊拼圖驗(yàn)證系統(tǒng)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot滑塊拼圖驗(yàn)證的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java判斷map是否為null或者空的方法小結(jié)

    Java判斷map是否為null或者空的方法小結(jié)

    這篇文章主要介紹了Java 如何判斷map為null或者空,文中通過代碼示例講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下
    2024-08-08
  • 用dom4j生成xml,去掉xml頭的方法

    用dom4j生成xml,去掉xml頭的方法

    今天小編就為大家分享一篇用dom4j生成xml,去掉xml頭的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2018-07-07
  • mybatis-xml映射文件及mybatis動(dòng)態(tài)sql詳解

    mybatis-xml映射文件及mybatis動(dòng)態(tài)sql詳解

    XML映射文件的名稱與Mapper接口名稱一致,并且將XML映射文件和Mapper接口放置在相同包下(同包同名),這篇文章主要介紹了mybatis-xml映射文件及mybatis動(dòng)態(tài)sql的相關(guān)知識(shí),感興趣的朋友跟隨小編一起看看吧
    2024-12-12
  • Java使用EasyExcel導(dǎo)出Excel的全流程詳解

    Java使用EasyExcel導(dǎo)出Excel的全流程詳解

    在 Java 后端開發(fā)中,Excel 導(dǎo)出是非常常見的需求,涉及報(bào)表、數(shù)據(jù)分析或業(yè)務(wù)導(dǎo)出,本文將結(jié)合 阿里 EasyExcel 庫,詳細(xì)講解 Excel 導(dǎo)出流程,包括工具類設(shè)計(jì)、樣式、列寬策略、對(duì)象映射、輸出流處理,需要的朋友可以參考下
    2025-11-11
  • Java設(shè)計(jì)模式之單例模式深入探索

    Java設(shè)計(jì)模式之單例模式深入探索

    單例模式(Singleton Pattern)是 Java 中最簡單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式,這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建
    2021-10-10
  • Java微信公眾號(hào)模板消息推送功能實(shí)例代碼

    Java微信公眾號(hào)模板消息推送功能實(shí)例代碼

    這篇文章主要介紹了Java微信公眾號(hào)模板消息推送功能的相關(guān)資料,包括了公眾號(hào)開發(fā)者平臺(tái)的參數(shù)獲取、微信發(fā)送接口的實(shí)際調(diào)用、消息發(fā)送的util類、示例模板以及接口調(diào)用的示例,需要的朋友可以參考下
    2025-05-05
  • Java 程序員掌握 Spring Boot非常有必要

    Java 程序員掌握 Spring Boot非常有必要

    本文帶領(lǐng)大家一起來了解下 Spring Boot 到底是什么?包括springboot的誕生,springboot特征具有哪些優(yōu)勢,如何讓開發(fā)變得更簡單,帶著這些問題一起通過本文學(xué)習(xí)下吧
    2021-06-06
  • 詳解JAVA中implement和extends的區(qū)別

    詳解JAVA中implement和extends的區(qū)別

    這篇文章主要介紹了詳解JAVA中implement和extends的區(qū)別的相關(guān)資料,extends是繼承接口,implement是一個(gè)類實(shí)現(xiàn)一個(gè)接口的關(guān)鍵字,需要的朋友可以參考下
    2017-08-08
  • Java表數(shù)據(jù)導(dǎo)出到Excel中的實(shí)現(xiàn)

    Java表數(shù)據(jù)導(dǎo)出到Excel中的實(shí)現(xiàn)

    這篇文章主要介紹了Java表數(shù)據(jù)導(dǎo)出到Excel中的實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-11-11
  • Java設(shè)計(jì)模式之抽象工廠模式詳解

    Java設(shè)計(jì)模式之抽象工廠模式詳解

    這篇文章主要介紹了Java設(shè)計(jì)模式之抽象工廠模式詳解,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下
    2021-05-05

最新評(píng)論

行唐县| 东阳市| 五河县| 开阳县| 岫岩| 高邮市| 鄂州市| 梧州市| 清远市| 历史| 海晏县| 衡阳市| 长治县| 贵德县| 巴林左旗| 阿鲁科尔沁旗| 定日县| 沂源县| 乌拉特中旗| 广宁县| 庆阳市| 乌拉特后旗| 常宁市| 拉萨市| 富川| 固安县| 新田县| 加查县| 吴川市| 肃北| 信阳市| 布拖县| 佳木斯市| 海安县| 项城市| 济南市| 界首市| 莲花县| 锡林郭勒盟| 湘西| 禹州市|