Vue3.0 PC端滑块拼图验证,配合后端验证
简介
最近因为产品需要实现一个滑块拼图验证,而且需要配合后端进行验证,不想接入第三方SDK,所以自己手写了一个,主要是配合element plus 实现UI大致框架,背景图片和拼图都是通过后端接口获取,通过CryptoJS对滑块滑动距离以及当前拼图唯一标识和秘钥进行加密处理,配合后端进行验证。 使用requestAnimationFrame 优化性能 为了防止滑块卡顿,有问题欢迎评论交流,另外还有小程序版本的 下次发布
安装CryptoJS库
yarn add crypto-js 或 npm i crypto-js
效果图
后端接口返回数据
获取图片的接口
背景图和拼图都是base64格式的
验证接口看后端怎么写
实现代码
<template>
<a-modal :visible="store.isVerify" :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) => {
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
}
const res = await userVerifyCaptcha(params)
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>