Vue + springboot实现拼图人机验证功能

 更新时间:2026年01月07日 17:25:39   作者:一只游鱼  
本文介绍了如何使用Vue和Spring Boot实现拼图人机验证功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

Vue + springboot实现拼图人机验证

接上个文章,我们扩展拼图人机验证功能。

一、实现方法

功能实现

  • 前端请求后端返回拼图验证码
  • 后端随机选择图片,随机扣图片的部分分为背景与图块
  • 将抠图的位置信息保存在redis中,并且将两类图片返回
  • 前端获得图片资源,将用户的拼图结果发给后端进行对比
  • 后端返回对比结果(这里也可以加上imageToken在登录时使用,更规范安全)

进阶(防脚本)

  • 后端抠图时扣两个不同位置的(一个真的一个假的),可以有效防止。

具体实现方式:

抠图时,redis只记录其中一个,且只保留这一个洞的拼图图片,返回给前端。

  • 轨迹校验

具体实现方式:

前端记录拼图的滑动轨迹,返回给后端校验人机。

二、实现依赖

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.25</version>
</dependency>

三、后端实现

抠图:

@GetMapping("/puzzle")
public Map<String, Object> puzzle() throws IOException {
    // 选择背景图
    String[] bgList = {"static/bg1.png", "static/bg2.png", "static/bg3.jpg"};
    String bgPath = bgList[ThreadLocalRandom.current().nextInt(bgList.length)];
    InputStream is = Thread.currentThread()
            .getContextClassLoader()
            .getResourceAsStream(bgPath);
    if (is == null) {
        throw new RuntimeException("未找到图片资源:" + bgPath);
    }
    BufferedImage bgOriginal = ImageIO.read(is);
    // 缩放到前端显示大小
    BufferedImage bg = new BufferedImage(
            BG_WIDTH,
            BG_HEIGHT,
            BufferedImage.TYPE_INT_ARGB
    );
    Graphics2D gResize = bg.createGraphics();
    gResize.drawImage(bgOriginal, 0, 0, BG_WIDTH, BG_HEIGHT, null);
    gResize.dispose();
    // 真的位置
    int realX = ThreadLocalRandom.current()
            .nextInt(60, BG_WIDTH - BLOCK_SIZE - 20);
    int realY = ThreadLocalRandom.current()
            .nextInt(20, BG_HEIGHT - BLOCK_SIZE - 20);
    // 生成假洞位置
    int fakeX;
    int fakeY;
    do {
        fakeX = ThreadLocalRandom.current()
                .nextInt(60, BG_WIDTH - BLOCK_SIZE - 20);
        fakeY = ThreadLocalRandom.current()
                .nextInt(20, BG_HEIGHT - BLOCK_SIZE - 20);
    } while (Math.abs(fakeX - realX) < BLOCK_SIZE
            && Math.abs(fakeY - realY) < BLOCK_SIZE);
    // 背景图挖两个洞(真 + 假)
    BufferedImage bgHole = new BufferedImage(
            BG_WIDTH,
            BG_HEIGHT,
            BufferedImage.TYPE_INT_ARGB
    );
    Graphics2D g = bgHole.createGraphics();
    g.drawImage(bg, 0, 0, null);
    // 开启透明抠除模式
    g.setComposite(AlphaComposite.Clear);
    // 真洞
    g.fillRect(realX, realY, BLOCK_SIZE, BLOCK_SIZE);
    // 假洞
    g.fillRect(fakeX, fakeY, BLOCK_SIZE, BLOCK_SIZE);
    g.dispose();
    // 生成真洞对应的拼图块
    BufferedImage block = new BufferedImage(
            BLOCK_SIZE,
            BLOCK_SIZE,
            BufferedImage.TYPE_INT_ARGB
    );
    Graphics2D g2 = block.createGraphics();
    g2.drawImage(bg, -realX, -realY, null);
    g2.dispose();
    // 保存图片
    String captchaId = UUID.randomUUID().toString();
    String bgFile = CAPTCHA_TEMP_DIR + captchaId + "_bg.png";
    String blockFile = CAPTCHA_TEMP_DIR + captchaId + "_block.png";
    ImageIO.write(bgHole, "png", new File(bgFile));
    ImageIO.write(block, "png", new File(blockFile));
    // Redis 保存真的
    redisTemplate.opsForValue().set(
            "captcha:" + captchaId,
            String.valueOf(realX),
            2,
            TimeUnit.MINUTES
    );
    // 返回前端
    return Map.of(
            "captchaId", captchaId,
            "bgUrl", fileurl + "/imageCaptcha/image?file=" + captchaId + "_bg.png",
            "blockUrl", fileurl + "/imageCaptcha/image?file=" + captchaId + "_block.png",
            "blockY", realY
    );
}

校验:

@PostMapping("/verify")
    public Result<?> verify(@RequestBody CaptchaVerifyVO dto) {
        if (dto.getCaptchaId() == null || dto.getCaptchaId().length() > 64) {
            return Result.error("250","参数非法");
        }
        String key = "captcha:" + dto.getCaptchaId();
        String realX = redisTemplate.opsForValue().get(key);
        if (realX == null) {
            return Result.error("250","验证码已过期");
        }
        int moveX = dto.getMoveX();
        int targetX = Integer.parseInt(realX);
        boolean positionOk = Math.abs(moveX - targetX) <= 5;
        boolean trackOk = checkTrack(dto.getTrack());
        if (!positionOk || !trackOk) {
            return Result.error("250","验证失败");
        }
        redisTemplate.delete(key);
//        // 生成令牌
//        String token = UUID.randomUUID().toString();
//        redisTemplate.opsForValue().set(
//                "captcha:token:" + token,
//                "1",
//                5,
//                TimeUnit.MINUTES
//        );
        return Result.success();
    }

轨迹检查方法

// 检查轨迹
    private boolean checkTrack(List<TrackPoint> track) {
        if (track == null || track.size() < 8) return false;
        int forward = 0;
        int backward = 0;
        for (int i = 1; i < track.size(); i++) {
            int diff = track.get(i).getX() - track.get(i - 1).getX();
            if (diff > 0) forward++;
            if (diff < 0) backward++;
        }
        // 大部分向前
        if (forward < track.size() * 0.6) return false;
        // 少量回拉
        if (backward > track.size() * 0.3) return false;
        return true;
    }

图片获取:

@GetMapping("/image")
public void getImage(@RequestParam String file, HttpServletResponse response) throws IOException {
    File f = new File(CAPTCHA_TEMP_DIR + file);
    if(!f.exists()) throw new RuntimeException("图片不存在");
    response.setContentType("image/png");
    try(FileInputStream fis = new FileInputStream(f)) {
        fis.transferTo(response.getOutputStream());
    }
}
  • 前端实现

子组件

<template>
  <div class="captcha-container">
    <div class="captcha-bg">
      <img :src="bgImage" class="bg-img" />
      <div
          class="block-img"
          :style="{
          left: blockLeft + 'px',
          top: blockY + 'px',
          width: blockSize + 'px',
          height: blockSize + 'px',
          backgroundImage: 'url(' + blockImage + ')'
        }"
          @mousedown.prevent="startDrag"
          @touchstart.prevent="startDrag"
      ></div>
    </div>
    <div class="slider-bar">
      <div
          class="slider-tip"
          v-show="!dragging"
      >
        滑动完成人机校验
      </div>
      <div
          class="slider-btn"
          :style="{ left: blockLeft + 'px', width: blockSize + 'px' }"
          @mousedown.prevent="startDrag"
          @touchstart.prevent="startDrag"
      >
        ➤
      </div>
    </div>
    <button class="refresh-btn" @click="refreshCaptcha">刷新验证码</button>
  </div>
</template>

加载图片:

// 加载验证码
async function loadCaptcha() {
  try {
    const res = await request.get("/imageCaptcha/puzzle");
    captchaId.value = res.data.captchaId;
    bgImage.value = res.data.bgUrl;
    blockImage.value = res.data.blockUrl;
    blockY.value = res.data.blockY;
    blockLeft.value = 0;
  } catch (err) {
    console.error("加载验证码失败", err);
  }
}

起始拖动:

function startDrag(e) {
  dragging = true;
  // 记录拖动起始点
  startX = e.type.includes("mouse")
      ? e.clientX
      : e.touches[0].clientX;
  // 记录拖动前滑块位置
  initialLeft = blockLeft.value;
  // 每次拖动开始时,清空轨迹
  track.value = [];
  // 初始化时间戳
  lastRecordTime = Date.now();
  document.addEventListener("mousemove", onDrag);
  document.addEventListener("mouseup", endDrag);
  document.addEventListener("touchmove", onDrag);
  document.addEventListener("touchend", endDrag);
}

拖动时:

function onDrag(e) {
  if (!dragging) return;
  // 当前鼠标 坐标
  const currentX = e.type.includes("mouse")
      ? e.clientX
      : e.touches[0].clientX;
  // 位移
  const deltaX = currentX - startX;
  // 计算新的滑块位置
  const newLeft = Math.max(
      0,
      Math.min(BG_WIDTH - BLOCK_SIZE, initialLeft + deltaX)
  );
  // 更新滑块位置
  blockLeft.value = newLeft;
  // 轨迹采集
  const now = Date.now();
  // 每 20ms 记录一次,模拟真人拖动
  if (now - lastRecordTime >= 20) {
    track.value.push({
      x: Math.round(newLeft), // 当前滑块 
      t: now                  // 时间戳
    });
    lastRecordTime = now;
  }
}

结束拖动:

async function endDrag() {
  if (!dragging) return;
  dragging = false;
  document.removeEventListener("mousemove", onDrag);
  document.removeEventListener("mouseup", endDrag);
  document.removeEventListener("touchmove", onDrag);
  document.removeEventListener("touchend", endDrag);
// 提交验证
  try {
    const res = await request.post("/imageCaptcha/verify", {
      captchaId: captchaId.value,
      // 最终滑块位置
      moveX: Math.round(blockLeft.value),
      // 提交轨迹数组
      track: track.value
    });
    if (res.data.code === "200") {
      ElMessage.success('校验成功')
      // 验证成功,通知父组件
      emit("success");
    } else {
      ElMessage.error('校验失败')
      emit("error")
    }
  } catch (err) {
    alert("验证失败,请重试!");
    blockLeft.value = 0;
    await loadCaptcha();
  }
}

父组件

<div v-if="showCaptcha" class="captcha-mask">
  <!-- 拼图验证码 -->
  <CaptchaSlider
      v-if="showCaptcha"
      ref="slider"
      @success="onCaptchaSuccess"
      @error="onCaptchaError"
  />
</div>
// 验证成功
const onCaptchaSuccess = () => {
  showCaptcha.value = false
  doLogin()  //这里是登录逻辑
}
const onCaptchaError = () => {
  showCaptcha.value = false
}

四、注意

后端生成的拼图的大小要和前端的一样,否则会出现错位的问题。

到此这篇关于Vue + springboot实现拼图人机验证的文章就介绍到这了,更多相关vue springboot验证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java线程实现时间动态显示

    Java线程实现时间动态显示

    这篇文章主要为大家详细介绍了Java线程实现时间动态显示,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-04-04
  • Spring Boot集成Druid出现异常报错的原因及解决

    Spring Boot集成Druid出现异常报错的原因及解决

    Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。本文讲述了Spring Boot集成Druid项目中discard long time none received connection异常的解决方法,出现此问题的同学可以参考下
    2021-05-05
  • spring的jdbctemplate的crud的基类dao

    spring的jdbctemplate的crud的基类dao

    本文主要介绍了使用spring的jdbctemplate进行增删改查的基类Dao的简单写法,需要的朋友可以参考下
    2014-02-02
  • Java实现文件读取和写入过程解析

    Java实现文件读取和写入过程解析

    这篇文章主要介绍了Java实现文件读取和写入过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值。,需要的朋友可以参考下
    2019-10-10
  • 常用数据库的驱动程序及JDBC URL分享

    常用数据库的驱动程序及JDBC URL分享

    这篇文章主要介绍了常用数据库的驱动程序及 JDBC URL,需要的朋友可以看下
    2014-01-01
  • Java字符串排序的几种实现方式

    Java字符串排序的几种实现方式

    这篇文章主要给大家介绍了关于Java字符串排序的几种实现方式, 使用Java平台进行字符串排序被认为是一件简单的工作,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-07-07
  • springboot Minio功能实现代码

    springboot Minio功能实现代码

    这篇文章主要介绍了springboot Minio功能实现,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-07-07
  • SpringBoot集成MyBatis对管理员的查询操作

    SpringBoot集成MyBatis对管理员的查询操作

    本文主要介绍了SpringBoot集成MyBatis对管理员的查询操作,实现增删改查中的查询操作,对所有的普通管理员进行查询操作,感兴趣的可以了解一下
    2023-11-11
  • Mybatis sqlMapConfig.xml中的mappers标签使用

    Mybatis sqlMapConfig.xml中的mappers标签使用

    这篇文章主要介绍了Mybatis sqlMapConfig.xml中的mappers标签使用方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教。
    2022-01-01
  • struts2配置静态资源代码详解

    struts2配置静态资源代码详解

    这篇文章主要介绍了struts2配置静态资源的相关内容,文中涉及了具体代码介绍,需要的朋友可以参考下。
    2017-09-09

最新评论