Vue3.0 PC端滑块拼图验证,配合后端验证,背景图片和拼图都是通过后端接口获取,通过CryptoJS对滑块滑动距离以及当前拼图唯一标识和秘钥进行加密处理,配合后端进行验证
安装CryptoJS库
yarn add crypto-js 或 npm i crypto-js
image.png
后端接口返回数据,背景图和拼图都是base64格式的
image.png
<template>
<!-- :visible="store.isVerify" -->
<a-modal
:open="true"
:footer="null"
:maskClosable="false"
:closable="true"
:body-style="{ padding: '20px' }"
@cancel="onClose"
:width="370"
style="top: 208px"
>
<div class="image-body">
<div class="verify-title">请先完成以下安全验证:</div>
<a-spin :spinning="loading" tip="加载中...">
<div v-if="loading" class="image-div"></div>
</a-spin>
<div v-if="!loading && bkImage && slideImage" class="image-div">
<img class="image-bk" :src="bkImage" />
<img
:style="{ marginLeft: marginLeft + 'px' }"
class="image-slide"
:src="slideImage"
/>
</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">
<a-button
:style="slideButtonStyle"
@mousedown="handleDrag"
class="slide-button"
type="primary"
>
<div class="iconbox">
<span
class="iconfont icon-jiantou"
v-if="result === 'default'"
style="color: #10b2bc; font-size: 16px"
>
</span
>
<span
class="iconfont"
v-if="result === 'success'"
style="color: #fff; font-size: 28px"
></span
>
<span
class="iconfont"
v-if="result === 'error'"
style="color: rgb(246, 185, 186)"
></span
>
</div>
</a-button>
</div>
</div>
</div>
</a-modal>
</template>
<script setup>
import { message } from "ant-design-vue";
// import { userGetCaptcha, userVerifyCaptcha } from "@/api/user";
// import { useMianStore } from "@/store/index";
import CryptoJS from "crypto-js";
// const store = useMianStore();
const loading = ref(false);
// 验证码背景图片
const bkImage = ref("");
// 验证码滑块图片
const slideImage = ref("");
const secretKey = ref("");
const currentToken = ref("");
// 是否显示提示文字
const showTips = ref(true);
// 验证码滑块图片移动量
const marginLeft = ref(0);
// 验证码状态
const result = ref("default");
// 滑动背景样式
const slideStyleJson = reactive({});
// 滑块按钮样式
const slideButtonStyleJson = reactive({});
const slideStyle = computed(() => {
return slideStyleJson;
});
const slideButtonStyle = computed(() => {
return slideButtonStyleJson;
});
function loadImage() {
loading.value = true;
// userGetCaptcha().then((res) => {
const res = {
data: {
token: "asadjl",
secret_key: "dsacsac",
slider_img_base_64:'xxx',
original_img_base_64: 'xxx'
},
};
loading.value = !loading.value;
bkImage.value = res.data.original_img_base_64;
slideImage.value = res.data.slider_img_base_64;
secretKey.value = res.data.secret_key;
currentToken.value = res.data.token;
// });
}
function onClose() {
// store.SET_ISVERIFY(false);
}
/**
* 改变拖动时改变
*/
function dragChangeSildeStyle() {
slideStyleJson.background = "rgba(25,145,250,0.5)";
slideStyleJson.transition = null;
slideButtonStyleJson.transition = "margin-left ease 0.5s";
}
/**
* 验证成功
*/
const reload = () => {
// store.SET_IS_RELOAD(false);
nextTick(() => {
// store.SET_IS_RELOAD(true);
});
};
function handleSuccess() {
result.value = "success";
slideStyleJson.background = "#d2f4ef";
slideButtonStyleJson.background = "#10b2bc";
slideButtonStyleJson.color = "white";
slideButtonStyleJson.border = "1px solid #10b2bc";
message.success("验证成功");
setTimeout(() => {
reload();
onClose();
}, 400);
}
/**
* 验证失败
*/
function handleError() {
result.value = "error";
slideStyleJson.background = "rgba(245,122,122,0.5)";
slideButtonStyleJson.background = "#f57a7a";
slideButtonStyleJson.transition = "transform 0.5s";
slideButtonStyleJson.transition = "margin-left ease 0.5s";
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.transform = "translateX(0px)";
slideButtonStyleJson.color = null;
slideButtonStyleJson.border = null;
slideButtonStyleJson.background = null;
slideStyleJson.transition = "all 0.5s ease";
slideButtonStyleJson.transition = "margin-left 0.5s ease";
showTips.value = true;
loadImage();
}
onMounted(() => {
loadImage();
});
// 添加移动事件
function handleDrag(c) {
let moveX = 0;
let offset = 0;
const y = 5;
const clickX = c.clientX;
dragChangeSildeStyle();
showTips.value = false;
const handleMove = (e) => {
moveX = e.clientX;
offset = Math.min(Math.max(moveX - clickX, 0), 260);
slideStyleJson.width = offset + "px";
slideButtonStyleJson.transform = `translateX(${offset}px)`;
marginLeft.value = offset;
};
const handleUp = async () => {
document.removeEventListener("mousemove", handleMove);
document.removeEventListener("mouseup", handleUp);
// 校验验证码
// aes加密
const key = CryptoJS.enc.Utf8.parse(secretKey.value);
const srcs = CryptoJS.enc.Utf8.parse(JSON.stringify({ x: offset, y: y }));
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
// 将ciphertext字符串转二进制
const params = {
point_json: CryptoJS.enc.Base64.stringify(encrypted.ciphertext),
token: currentToken.value,
};
console.log(params, "params");
// const res = await userVerifyCaptcha(params);
const res = {
code: 200,
};
if (res.code === 200) {
// 成功
handleSuccess();
} else {
// 失败
handleError();
}
};
document.addEventListener("mousemove", handleMove);
document.addEventListener("mouseup", handleUp);
requestAnimationFrame(() => {
handleMove(c);
});
}
// 移除监听事件
onUnmounted(() => {
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("mouseup", handleDrag);
});
</script>
<style lang="scss" scoped>
.iconbox {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.iconfont {
font-size: 20px;
color: #333;
}
.image-body {
margin: 0 auto;
width: 310px;
padding-bottom: 20px;
.verify-title {
font-size: 14px;
color: #333;
margin-bottom: 10px;
}
.image-div {
// position: relative;
width: 310px;
height: 155px;
background: rgb(153, 216, 197);
box-shadow: 0 0 4px #ccc;
.image-bk {
width: 310px;
height: 155px;
z-index: 1;
position: absolute;
}
.image-slide {
width: 50px;
height: 155px;
position: absolute;
z-index: 2;
}
}
.image-slide-div {
width: 310px;
height: 39px;
margin-top: 15px;
position: relative;
.image-slide-text {
text-align: center;
background: #eef1f8;
border: 1px solid #ebebeb;
.image-slide-tips {
display: inline-block;
font-size: 14px;
color: #b7bcd1;
line-height: 36px;
height: 36px;
text-align: center;
}
}
.slide-div {
width: 0px;
height: 38px;
margin-top: -38px;
.slide-button {
width: 50px;
height: 38px;
border: none;
border-left: 1px solid;
border-right: 1px solid;
border-bottom: 1px solid;
border-color: #ebebeb;
box-shadow: 0 0 4px #ccc;
background: white;
cursor: pointer;
&:hover {
background: #10b2bc;
border-color: #10b2bc;
color: white;
.icon-jiantou {
color: #fff !important;
}
}
}
}
}
}
</style>
用到的图片地址:
https://necaptcha.nosdn.127.net/1408a0ca90b8420684d00878c2c84fde@2x.png
https://necaptcha.nosdn.127.net/c5e34ba0cc704eb8ba013d9ff22355ee@2x.jpg
在线图片转Base64字符
https://www.zhihuilib.com/general/imagetobase64