您的当前位置:首页正文

Java+vue实现滑动拼图验证码简单实现

2024-11-19 来源:个人技术集锦

效果:

1、环境

        jdk1.8 springboot 2.7  vue

2、验证码图片生成类

package my.verify.slide.utils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import static java.awt.Color.BLACK;
import static java.awt.Transparency.TRANSLUCENT;

/**
 * 验证码绘制类
 */
public class DrawCaptchaUtil {
    /**
     * 画布宽度
     */
    private static int canvasWidth = 300;

    /**
     * 画布高度
     */
    private static int canvasHeight = 150;
    /**
     * 图片宽度
     */
    private static int imageWidth = 300;

    /**
     * 图片高度
     */
    private static int imageHeight = 150;
    /**
     * 滑块画布宽度
     */
    private static int slideCanvasWidth = 50;
    /**
     * 滑块画布高度
     */
    private static int slideCanvasHeight = 150;
    /**
     * 滑块图片宽度
     */
    private static int slideImageWidth = 50;
    /**
     * 滑块图片高度
     */
    private static int slideImageHeight = 50;

    /**
     * 绘制背景图片
     *
     * @param imagePath      图片路径
     * @param slideImagePath 滑块图片路径
     * @param code 存放验证码信息
     * @return
     * @throws IOException
     */
    public static Map<String, String> drawImage(File imagePath, File slideImagePath,HashMap<String,Integer> code) throws IOException {
        BufferedImage canvas = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = (Graphics2D) canvas.getGraphics();
        g.fillRect(0, 0, imageWidth, imageHeight);
        BufferedImage read = ImageIO.read(Files.newInputStream(Paths.get(imagePath.getAbsolutePath())));
        g.drawImage(read, 0, 0, null, null);
        int[] point = randomAnchorPoint();
        //绘制滑块图片
        BufferedImage slideImage = drawSlideImage(slideImagePath, canvas, point[0], point[1]);
        //设置为透明覆盖 很重要
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.9f));
        //覆盖阴影
        g.drawImage(drawSlideImageShadow(slideImagePath), point[0], point[1], null, null);
        g.dispose();
        HashMap<String, String> images = new HashMap<>();
        images.put("bkImage", ImageToBase64Util.bufferedImageToBase64(canvas));
        images.put("slideImage", ImageToBase64Util.bufferedImageToBase64(slideImage));
        //存放验证码
        code.put("offset",point[0]);
        return images;
    }

    /**
     * 绘制滑块
     *
     * @param slideImagePath 滑块图片路径
     * @param bkImg          验证码背景图
     * @param x              随机坐标X
     * @param y              随机坐标Y
     * @return
     * @throws IOException
     */
    private static BufferedImage drawSlideImage(File slideImagePath, BufferedImage bkImg, int x, int y) throws IOException {
        BufferedImage canvas = new BufferedImage(slideCanvasWidth, slideCanvasHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = canvas.createGraphics();
        g.getDeviceConfiguration().createCompatibleImage(slideCanvasWidth, slideCanvasHeight, Transparency.TRANSLUCENT);
        g = canvas.createGraphics();
        BufferedImage slideImage = bkImg.getSubimage(x, y, slideImageWidth, slideImageHeight);
        BufferedImage slide = ImageIO.read(Files.newInputStream(Paths.get(slideImagePath.getAbsolutePath())));
        Graphics2D g2 = slide.createGraphics();
        //设置为透明覆盖 很重要
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
        g2.drawImage(slideImage, 0, 0, null);
        g2.dispose();
        g.drawImage(slide, 0, y, null);
        g.dispose();
        return canvas;
    }

    /**
     * 绘制背景图上的阴影
     *
     * @param slideImagePath
     * @return
     * @throws IOException
     */
    private static BufferedImage drawSlideImageShadow(File slideImagePath) throws IOException {
        BufferedImage slide = ImageIO.read(Files.newInputStream(Paths.get(slideImagePath.getAbsolutePath())));
        return slide;

    }


    /**
     * 生成随机坐标点 x > 滑块画布宽度 y < 滑块画布高度-滑块图片高度
     *
     * @return
     */
    private static int[] randomAnchorPoint() {
        Random random = new Random();
        //设置x坐标从 slideCanvasWidth 开始至 canvasWidth - slideCanvasWidth 范围随机
        int x = random.nextInt(canvasWidth - slideImageWidth);
        if (x < slideImageWidth) {
            //随机生成x点小于图片宽度+上图片宽度
            x += slideCanvasWidth;
        }
        //设置y坐标
        int y = random.nextInt(canvasHeight - slideImageHeight);
        int[] point = {x, y};
        return point;
    }


}

3、BufferedImage转BASE64类

package my.verify.slide.utils;

import sun.misc.BASE64Encoder;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class ImageToBase64Util {

    /**
     * BufferedImage 对象转BASE64
     * @param image
     * @return
     */
    public static String bufferedImageToBase64(BufferedImage image)  {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            ImageIO.write(image,"PNG",out);
            byte[] bytes = out.toByteArray();
            BASE64Encoder base64Encoder = new BASE64Encoder();
            return base64Encoder.encodeBuffer(bytes);
        } catch (IOException e) {
            try {
                if(out != null){
                    out.close();
                }
            } catch (IOException ex) {

            }
        }

        return null;
    }


}

4、前端实现

<template>
  <div class="image-body">
    <div class="image-div">
      <ElImage class="image-bk" :src="bkImage"></ElImage>
      <ElImage
        :style="{ marginLeft: marginLeft + 'px' }"
        class="image-slide"
        :src="slideImage"
      ></ElImage>
    </div>
    <div class="image-slide-div">
      <div class="image-slide-text">
        <span class="image-slide-tips">
          {{ showTips ? "向右拖动滑块填充拼图" : "&nbsp;" }}
        </span>
      </div>
      <div :style="slideStyle" class="slide-div">
        <ElButton
          :style="slideButtonStyle"
          @mousedown="handleDrag"
          class="slide-button"
          :round="false"
          type="primary"
          plain
        >
          <template #icon>
            <Right v-if="result === 'default'" />
            <Check v-if="result === 'success'" />
            <Close v-if="result === 'error'" />
          </template>
        </ElButton>
      </div>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, computed, reactive } from "vue";
import { getSlideImage, verifyCodeCheck } from "@/assets/api/api";
import { ElImage, ElIcon, ElButton } from "element-plus";
import { Right, Check, Close } from "@element-plus/icons-vue";
export default defineComponent({
  components: { ElImage, ElIcon, Right, Check, Close, ElButton },
  setup() {
    //验证码背景图片
    const bkImage = ref("");
    //验证码滑块图片
    const slideImage = ref("");
    //是否显示提示文字
    const showTips = ref(true);
    //验证码滑块图片移动量
    const marginLeft: any = ref(0);
    //验证码状态
    const result: any = ref("default");
    //滑动背景样式
    let slideStyleJson: any = reactive({});
    //滑块按钮样式
    let slideButtonStyleJson: any = reactive({});
    const slideStyle = computed(() => {
      return slideStyleJson;
    });
    const slideButtonStyle = computed(() => {
      return slideButtonStyleJson;
    });

    function loadImage() {
      getSlideImage().then((res: any) => {
        bkImage.value = "data:image/png;base64," + res.bkImage;
        slideImage.value = "data:image/png;base64," + res.slideImage;
      });
    }
    /**
     * 改变拖动时改变
     */
    function dragChangeSildeStyle() {
      slideStyleJson.background = "rgba(25,145,250,0.5)";
      slideStyleJson.transition = null;
      slideButtonStyleJson.transition = null;
    }
    /**
     * 验证成功
     */
    function handleSuccess() {
      result.value = "success";
      slideStyleJson.background = "#d2f4ef";
      slideButtonStyleJson["background"] = "#52ccba";
      slideButtonStyleJson["color"] = "white";
      slideButtonStyleJson["border"] = "1px solid #52ccba";
    }
    /**
     * 验证失败
     */
    function handleError() {
      result.value = "error";
      slideStyleJson.background = "rgba(245,122,122,0.5)";
      slideButtonStyleJson["background"] = "#f57a7a";
      slideButtonStyleJson["color"] = "white";
      slideButtonStyleJson["border"] = "1px solid #f57a7a";
      setTimeout(() => {
        handleReset();
      }, 300);
    }
    /**
     * 重置验证码
     */
    function handleReset() {
      result.value = "default";
      marginLeft.value = 0;
      slideStyleJson.width = "0px";
      slideButtonStyleJson.marginLeft = "0px";
      slideButtonStyleJson.color = null;
      slideButtonStyleJson.border = null;
      slideButtonStyleJson.background = null;
      slideStyleJson.transition = "width 0.5s";
      slideButtonStyleJson.transition = "margin-left 0.5s";
      showTips.value = true;
      loadImage();
    }

    onMounted(() => {
      loadImage();
    });
    //添加移动事件
    function handleDrag(c: any) {
      let clickX = c.clientX;
      dragChangeSildeStyle();
      showTips.value = false;
      document.onmousemove = function (e: any) {
        let moveX = e.clientX;
        let offset: any = moveX - clickX;
        if (offset < 0) {
          offset = 0;
        } else if (offset > 250) {
          offset = 250;
        }
        let slidePadding: any = (offset / 25).toFixed(0);
        let slideDivOffset = offset + parseInt(slidePadding) + "px";
        slideStyleJson.width = slideDivOffset;
        slideButtonStyleJson.marginLeft = slideDivOffset;
        marginLeft.value = offset;
      };
      document.onmouseup = async function () {
        document.onmousemove = null;
        document.onmouseup = null;
        //校验验证码
        const res:any = await verifyCodeCheck({ offset: marginLeft.value });
        if (res) {
          //成功
          handleSuccess();
        } else {
          //失败
          handleError();
        }
      };
    }
    return {
      loadImage,
      bkImage,
      slideImage,
      marginLeft,
      Right,
      Close,
      Check,
      handleDrag,
      slideStyle,
      slideButtonStyle,
      result,
      showTips,
    };
  },
});
</script>
<style lang="less" scoped>
.image-body {
  margin: 0 auto;
  width: 300px;
  .image-div {
    width: 300px;
    height: 150px;
    background: rgb(153, 216, 197);
    .image-bk {
      width: 300px;
      height: 150px;
      z-index: 1;
      position: absolute;
    }
    .image-slide {
      width: 50px;
      height: 150px;
      position: absolute;
      z-index: 2;
    }
  }
  .image-slide-div {
    width: 300px;
    height: 38px;
    margin-top: 15px;
    position: relative;
    .image-slide-text {
      background: #f7f9fa;
      border: 1px solid #ebebeb;
      .image-slide-tips {
        font-size: 14px;
        line-height: 38px;
        text-align: center;
      }
    }
    .slide-div {
      width: 0px;
      height: 38px;
      margin-top: -39px;
      .slide-button {
        width: 40px;
        height: 38px;
        border: none;
        border-left: 1px solid;
        border-right: 1px solid;
        border-color: #ebebeb;
        background: white;
        cursor: pointer;
        &:hover {
          background: #1991fa;
          border-color: #1991fa;
          color: white;
        }
      }
    }
  }
}
</style>

源码链接:前端

                  后端

显示全文