效果:
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 ? "向右拖动滑块填充拼图" : " " }}
</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>
源码链接:前端
后端