uniapp and vue implement sliding puzzle verification code

uniapp and vue implement sliding puzzle verification code


In actual development work, SMS verification code is required when logging in, but it is easy to cause crawler behavior, so anti-crawler verification code needs to be used. Today I will introduce the puzzle verification code to solve the sliding verification code anti-crawler in verification code anti-crawler. The sliding puzzle verification code adds a random sliding distance to the sliding block verification code. The user needs to slide the slider to the gap in the puzzle to complete the puzzle in order to pass the verification. Platform compatibility, H5, WeChat applet, Byte, Baidu, QQ, etc.

In actual development work, SMS verification code is required when logging in, but it is easy to cause crawler behavior, so anti-crawler verification code needs to be used. Today I will introduce the puzzle verification code to solve the sliding verification code anti-crawler in verification code anti-crawler.
Principle:
The sliding puzzle verification code adds a random sliding distance to the sliding block verification code. The user needs to slide the slider to the gap of the puzzle to make the puzzle complete in order to pass the verification.


1. Incorrect interface version

Platform compatibility, H5, WeChat applet, Byte, Baidu
XXX. vue file is introduced
in the template tag

<slider-verify :isShow="sliderVerifyFLag" @touchSliderResult="verifyResult" ref="verifyElement"></slider-verify>

Introduce components into script tags

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{
                // 校验失败,点击关闭按钮
            }
        }
    }
};

Insert image description here
slider-verify.vue file code

<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>

2. Interface version
Insert image description here

Platform compatibility, H5, WeChat applet, Byte, Baidu, qq, etc. XXX.vue
files are introduced
into the template tag

<view @tap="getCode()" >获取验证码</view>
<validationPT class="setVef" ref="vefCode" @VefCodeTrue="getVefCodeTrue" :inputPhone="mobile" theme="dialog"></validationPT>

Introduce components into script tags

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 file code

<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>

Guess you like

Origin blog.csdn.net/meimeieee/article/details/133255621