uniapp、vue实现滑动拼图验证码
实际开发工作中,在登陆的时候需要短信验证码,但容易引起爬虫行为,需要用到反爬虫验证码,今天介绍一下拼图验证码,解决验证码反爬虫中的滑动验证码反爬虫。滑动拼图验证码是在滑块验证码的基础上增加了一个随机滑动距离,用户需要将滑块滑到拼图的缺口处,使拼图完整,才能通过校验。平台兼容性,H5、微信小程序、字节、百度、qq等。
实际开发工作中,在登陆的时候需要短信验证码,但容易引起爬虫行为,需要用到反爬虫验证码,今天介绍一下拼图验证码,解决验证码反爬虫中的滑动验证码反爬虫。
原理
滑动拼图验证码是在滑块验证码的基础上增加了一个随机滑动距离,用户需要将滑块滑到拼图的缺口处,使拼图完整,才能通过校验。
一、不对接口版本
平台兼容性,H5、微信小程序、字节、百度
XXX.vue文件引入
template标签中引入
<slider-verify :isShow="sliderVerifyFLag" @touchSliderResult="verifyResult" ref="verifyElement"></slider-verify>
script标签中引入组件
import sliderVerify from '@/components/slider-verify/slider-verify.vue';//组件存放的路径
export default {
components: {
'slider-verify': sliderVerify
},
data() {
return {
sliderVerifyFLag: false //滑块验证
};
},
onLoad() {},
methods: {
// 滑块验证结果回调函数
verifyResult(res) {
this.sliderVerifyFLag = false;
if (res) { //校验通过
}else{
// 校验失败,点击关闭按钮
}
}
}
};
slider-verify.vue文件代码
<template>
<view class="slider-verify-box" v-if="isShow">
<view class="verifyBox">
<view class="slider-title">图形验证</view>
<view class="slide-content">
<view class="slide-tips">拖动下方滑块完成拼图</view>
<view class="slider-pintu">
<image id="pintuImg" :src="'/static/images/slider-verify/' + img + '.jpg'" class="pintu"></image>
<view class="pintukuai" :style="{ top: top + 'px', left: oldx + 'px' }">
<image :src="'/static/images/slider-verify/' + img + '.jpg'" :style="{ top: '-' + top + 'px', left: '-' + left + 'px'}"></image>
</view>
<view class="yinying" :style="{ top: top + 'px', left: left + 'px' }"></view>
</view>
<view class="slider-movearea" @touchend="endTouchMove">
<movable-area :animation="true"><movable-view :x="x" direction="horizontal" @change="startMove"></movable-view></movable-area>
<view class="huadao">拖动左边滑块完成上方拼图</view>
</view>
</view>
<view class="slider-btn-group">
<view class="slider-btn" @tap="closeSlider">关闭</view>
<view class="slider-btn slide-btn-refresh" @tap="refreshVerify">刷新</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'slider-verify',
props: {
isShow: true
},
data() {
return {
x: 0, //初始距离
oldx: 0, //移动的距离
img: '1', //显示哪张图片
left: 0, //随机拼图的最终X轴距离
top: 0, //拼图的top距离
};
},
watch: {
// 每次打开重新刷新拼图
isShow(newValue, oldValue) {
if(newValue){
this.refreshVerify(); //刷新
}
}
},
mounted() {
var that = this;
that.refreshVerify();
},
methods: {
//刷新验证
refreshVerify() {
var gl = Math.random().toFixed(2);
this.left = uni.upx2px(560) * gl > uni.upx2px(280) ? uni.upx2px(280) : uni.upx2px(560) * gl + uni.upx2px(150); //生成随机X轴最终距离
this.top = uni.upx2px(190) * gl; //生成随机Y轴初始距离
if (gl <= 0.2) {
this.img = 1;
}
if (gl > 0.2 && gl <= 0.4) {
this.img = 2;
}
if (gl > 0.4 && gl <= 0.6) {
this.img = 3;
}
if (gl > 0.6 && gl <= 0.8) {
this.img = 4;
}
if (gl > 0.8 && gl <= 1) {
this.img = 5;
}
this.resetMove(); //重置阴影位置
},
/* 滑动中 */
startMove(e) {
this.oldx = e.detail.x;
},
/* 滑动结束 */
endTouchMove() {
var that = this;
if (Math.abs(that.oldx - that.left) <= 5) {
uni.showToast({
title: '验证成功',
duration: 2500,
success() {
that.$emit('touchSliderResult', true);
}
});
} else {
that.refreshVerify();
}
},
/* 重置阴影位置 */
resetMove() {
this.x = 1;
this.oldx = 1;
setTimeout(() => {
this.x = 0;
this.oldx = 0;
}, 300);
},
// 关闭
closeSlider(){
this.$emit('touchSliderResult', false);
}
}
};
</script>
<style lang="less">
.slider-verify-box {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.verifyBox {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
// width: 85%;
background-color: #fff;
border-radius: 20upx;
box-shadow: 0 0 5upx rgba(0, 0, 0);
.slider-title {
font-size: 36upx;
text-align: center;
padding: 1em 0;
color: rgba(2, 20, 33, 0.85);
border-bottom: 1px solid rgba(2, 20, 33, 0.15);
}
.slide-content {
width: 560rpx;
padding: 0 1em;
margin: 0 auto;
.slide-tips {
font-size: 28rpx;
color: rgba(2, 20, 33, 0.45);
padding: 0.5em 0;
}
.slider-pintu {
position: relative;
width: 100%;
border-radius: 10rpx;
overflow: hidden;
.pintu {
width: 560rpx;
height: 315rpx;
display: block;
margin: 0 auto;
}
.pintukuai {
position: absolute;
top: 0;
left: 0;
width: 120rpx;
height: 120rpx;
z-index: 100;
box-shadow: 0 0 5upx rgba(0, 0, 0, 0.3);
overflow: hidden;
image {
display: block;
position: absolute;
top: 0;
left: 0;
width: 560rpx;
height: 315rpx;
}
}
}
.yinying {
position: absolute;
width: 120rpx;
height: 120rpx;
background-color: rgba(0, 0, 0, 0.5);
}
}
}
.slider-movearea {
position: relative;
height: 80upx;
width: 100%;
margin: 25upx auto;
movable-area {
height: 80upx;
width: 100%;
movable-view {
width: 80upx;
height: 80upx;
border-radius: 50%;
background-color: #007cff;
background-image: url(../../static/images/slider-verify/icon-button-normal.png);
background-repeat: no-repeat;
background-size: auto 30upx;
background-position: center;
position: relative;
z-index: 100;
}
}
}
.huadao {
width: 100%;
height: 66upx;
line-height: 66upx;
background: #eee;
box-shadow: inset 0 0 5upx #ccc;
border-radius: 40rpx;
color: #999;
text-align: center;
box-sizing: border-box;
position: absolute;
top: 7rpx;
left: 0;
font-size: 28rpx;
z-index: 99;
}
.slider-btn-group{
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border-top: 1px solid rgba(2, 20, 33, 0.15);
.slider-btn{
flex: 1;
height: 100rpx;
line-height: 100rpx;
text-align: center;
font-size: 36rpx;
color:rgba(2,20,33,0.85);
&:active{
opacity: 0.8;
}
}
.slide-btn-refresh{
color:rgba(14,107,176,1);
border-left: 1px solid rgba(2, 20, 33, 0.15);
}
}
</style>
二、对接口版本
平台兼容性,H5、微信小程序、字节、百度、qq等
XXX.vue文件引入
template标签中引入
<view @tap="getCode()" >获取验证码</view>
<validationPT class="setVef" ref="vefCode" @VefCodeTrue="getVefCodeTrue" :inputPhone="mobile" theme="dialog"></validationPT>
script标签中引入组件
import validationPT from '@/components/validationPT/validationPT.vue'
export default {
components: {
'slider-verify': sliderVerify
},
data() {
},
onLoad() {},
methods: {
// 效验手机号
getCode() {
this.getCodePT();
},
//获取弹窗
getCodePT(msg) {
this.$refs.vefCode.GetSlideBlockApi(); //调用图片接口,获取验证图片
this.$refs.vefCode.clkOpenRef(); //验证通过,打开拼图验证
this.$refs.vefCode.initial();//重置
},
// 滑块验证结果回调函数
//拼图验证是否成功
getVefCodeTrue(msg) {
this.VefInfosBk = msg;
if (msg.Res == true) {
//获取验证码
}
},
}
};
validationPT.vue文件代码
<template>
<view :class="[{'frame--dialog':theme}]">
<view class="frameBg" v-show="showModal" style="z-index:10"></view>
<view class="framework" v-if="isVefCode">
<view class="boxTopTitle">
<text>图形验证码</text>
<text class="cuIcon-close font16 fontB" @click="clkCloseRef"></text>
</view>
<view class="boxImg">
<view class="cutImgSet" :class="isAnimation?'animation':''"
:style="{top:CutImgY+'px', left: blockLeft + 'px'}">
<image :src="CutImg" style="cursor:pointer; width: 40px;height: 40px;z-index: 10;"></image>
</view>
<image :src="BGImg" style="border-radius:8px; width: 300px;height: 150px;"></image>
<view class="reset" @click="clkNext">
<text class="cuIcon-refresh font16 text-white"></text>
</view>
</view>
<view class="checkBox">
<view class="checkBar">
<view class="slide" :class="bgColSet?'active':''">
<view :class="'moveBac '+(isAnimation?' animation':'')+(bgColSet?' moveBacError':'')"
:style="'width:'+(isSuccess ? 300 : blockLeft)+'px;'"></view>
<view :class="(bgColSet?'swiperTipsError':'swiperTips') + (!isSuccess?' paddingL40':'')">
<text v-if="!isSuccess">请拖动滑块验证</text>
<text v-if="isSuccess" class="cuIcon-check margin-right-sm fontB"></text>
<text v-if="isSuccess">验证成功</text>
</view>
<view v-if="!isSuccess"
:class="'swiperBlock '+(bgColSet?' errorBlock':' successBlock')+(isAnimation?' animation':'') "
:style="'left:'+blockLeft+'px'" ref="sliderBtn" @touchstart="touchstartHandle"
@mousedown="startMove" @touchmove.stop.prevent="touchmoveHandle"
@touchend="touchendHandle">
<!-- #ifndef MP-BAIDU -->
<image v-if="!bgColSet" :src="ImgUrl + 'Images/doubleArrow.svg'" mode="widthFix"
style="width: 16px;"></image>
<text v-else class="cuIcon-close font16 text-white"></text>
<!-- #endif -->
<!-- #ifdef MP-BAIDU -->
<text v-if="bgColSet" class="cuIcon-close font16 text-white"></text>
<!-- #endif -->
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
inputPhone: '',
theme: {
type: String,
},
swiperColor: {
type: String,
default: 'rgba(21, 132, 223, 0.08)'
},
title: {
type: String,
default: '人机校验'
},
barWidth: {
type: Number,
default: 300
}
},
data() {
return {
ImgUrl: this.hostwapUrl,
BGImg: '',
CutImg: '',
CutImgY: '',
MarkCode: '', //验证拼图是否成功用,
bgColSet: false, //拼图是否验证成功
isSuccess: false, //是否验证成功
isVefCode: false,
showModal: false,
startInfo: {},
blockLeft: 0,//随机拼图的最终X轴距离
isAnimation: false,
msgType: '',
}
},
computed: {
trueMoveableW: function() {
return this.barWidth - 40
}
},
methods: {
//获取图片接口
GetSlideBlockApi() {
let that = this;
uni.request({
url: url,//获取拼图接口
data: data,//需要传给接口的参数
headers: {
Accept: "application/json; charset=utf-8"
},
dataType: 'json',
method: 'GET',
success: (res) => {
this.BGImg = '';//大图
this.CutImg = '';//拼图
this.CutImgY = 0;//接口位置
},
complete: () => {}
})
},
//手指按下
touchstartHandle({
changedTouches
}) {
if (this.isSuccess) {
return
}
this.isAnimation = false
this.startInfo = changedTouches[0]
},
// 手指移动
touchmoveHandle({
changedTouches
}) {
if (this.isSuccess) {
return
}
let blockLeft = changedTouches[0].clientX - this.startInfo.clientX
let blockLeftRpx = blockLeft;
if (blockLeftRpx < 0) {
this.blockLeft = 0
} else {
this.blockLeft = blockLeftRpx <= this.trueMoveableW ? blockLeftRpx : this.trueMoveableW
}
},
// 手指离开
touchendHandle(e) {
if (this.isSuccess) {
return
}
this.CheckSlideBlockApi();
},
//验证图片接口
CheckSlideBlockApi() {
let that = this;
uni.request({
url: url,//接口名称
data: data,//接口需要的参数
headers: {
Accept: "application/json; charset=utf-8"
},
dataType: 'json',
method: 'POST',
success: (res) => {
let infos = {}
if (res.data.IsOK == true) {//成功
that.isSuccess = true;
infos = {
Res: res.data.Results,
};//返回传给父组件的值,判断是否成功
setTimeout(() => {
that.bgColSet = false;
that.$emit('VefCodeTrue', infos)
that.isVefCode = false;
that.showModal = false;
}, 1000);
} else {
that.bgColSet = true;
that.isSuccess = false;
that.GetSlideBlockApi();
that.isAnimation = true
let timeid = setTimeout(() => {
clearTimeout(timeid)
that.isAnimation = false
that.bgColSet = false;
}, 500)
that.blockLeft = 0;
}
},
complete: () => {}
})
},
/* 鼠标按住滑块后初始化移动监听,记录初始位置 */
startMove(e) {
e.preventDefault() //禁止图片img拖动的解决方法
e = e || window.event;
this.moveStart = e.pageX || e.targetTouches[0].pageX;
this.addMouseMoveListener();
},
/* 鼠标滑块移动 */
moving(e) {
// e.preventDefault() //禁止图片img拖动的解决方法
let self = this;
e = e || window.event;
let moveX = (e.pageX || e.targetTouches[0].pageX);
let d = moveX - self.moveStart;
let w = self.dataWidth;
let PL_Size = this.puzzleSize;
let padding = this.padding;
if (self.moveStart === "") {
return "";
}
if (d < 0 || d > w - padding - PL_Size) {
return "";
}
if (d <= 260) {
self.blockLeft = d
}
},
/* 鼠标移动结束,验证并回调 */
moveEnd(e) {
let self = this;
e = e || window.event;
let moveEnd_X = (e.pageX || e.changedTouches[0].pageX) - self.moveStart;
if (moveEnd_X > 0) {
self.CheckSlideBlockApi(); //验证拼图是否成功
}
self.moveStart = "";
document.removeEventListener("mousemove", self.moving);
document.removeEventListener("mouseup", self.moveEnd);
},
/* 鼠标全局绑定滑块移动与滑动结束,移动过程中鼠标可在页面任何位置 */
addMouseMoveListener() {
let self = this;
document.addEventListener("mousemove", self.moving);
document.addEventListener("mouseup", self.moveEnd);
},
//换一张
clkNext() {
this.GetSlideBlockApi();
},
// 重置滑块位置
initial() {
this.blockLeft = 0;
this.bgColSet = false;
this.isSuccess = false;
},
clkCloseRef() {
this.showModal = false;
this.isVefCode = false;
},
clkOpenRef(msg) {
this.showModal = true;
this.isVefCode = true;
},
}
}
</script>
<style lang="scss" type="text/css" rel="stylesheet" scoped="scoped">
.framework {
box-sizing: border-box;
width: 332px;
height: 270px;
background: #fff;
margin: 24px auto;
z-index: 11;
position: relative;
padding: 0 16px;
user-select: none;
border-radius: 16px;
}
.framework .boxTopTitle {
height: 48px;
line-height: 48px;
display: flex;
justify-content: space-between;
}
.framework .boxImg {
width: 300px;
height: 150px;
background: #fff;
margin-bottom: 16px;
border-radius: 8px;
position: relative;
}
.reset {
position: absolute;
top: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.24);
border-radius: 0 8px 0 8px;
padding: 0 12px;
line-height: 32px;
}
.cutImgSet {
position: absolute;
}
.frame--dialog {
.framework {
// margin: 38vh auto;
z-index: 88888888;
position: fixed;
// top: 25vh;
transform: translateX(-50%);
left: 50%;
}
.frameBg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.24);
z-index: 8887 !important;
}
}
.checkBox .checkBar {
width: 100%;
padding: 0px;
}
.slide {
box-sizing: border-box;
width: 100%;
height: 40px;
line-height: 40px;
border-radius: 8px;
background-color: #FFFFFF;
position: relative;
font-size: 13px;
overflow: hidden;
border: 1px solid rgba(65, 157, 231, 0.56);
}
.slide.active {
border: 1px solid #FA7F7F;
}
.moveBac {
background-color: rgba(21, 132, 223, 0.08);
width: 100%;
height: 100%;
}
.moveBacError {
background-color: rgba(216, 63, 63, 0.08) !important;
}
.swiperTips {
box-sizing: border-box;
position: absolute;
left: 0;
top: 0;
width: 100%;
color: rgba(255, 255, 255, 0.24);
text-align: center;
background: -webkit-gradient(linear, left top, right top, color-stop(0, #0076D6), color-stop(.4, #0076D6), color-stop(.5, #fff), color-stop(.6, #0076D6), color-stop(1, #0076D6));
animation: tipsBlinkan 3s infinite;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 40px;
}
.swiperTipsError {
box-sizing: border-box;
position: absolute;
left: 0;
top: 0;
width: 100%;
text-align: center;
color: rgba(255, 255, 255, 0.24);
background: -webkit-gradient(linear, left top, right top, color-stop(0, #FA7F7F), color-stop(.4, #FA7F7F), color-stop(.5, #fff), color-stop(.6, #FA7F7F), color-stop(1, #FA7F7F));
animation: tipsBlinkan 3s infinite;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 40px;
}
.swiperBlock {
width: 40px;
height: 40px;
border-radius: 8px;
/* #ifdef MP-BAIDU */
background-repeat: no-repeat;
background-size: 16px;
background-position: center;
/* #endif */
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0px;
top: -1px;
}
.successBlock {
background-color: #0076D6;
/* #ifdef MP-BAIDU */
background-image: url('https://m.by56.com/Images/doubleArrow.svg');
/* #endif */
}
.errorBlock {
background-color: #FA7F7F !important;
}
.paddingL40 {
padding-left: 40px;
}
.animation {
transition: all 0.5s;
}
@keyframes tipsBlinkan {
0% {
background-position: -100px 0;
}
100% {
background-position: 100px 0;
}
}
</style>