前言

  • 滑块验证不只判断是否滑动到尾部,真正的目的是检测用户行为,检测行为是人为、脚本、还是其它。

  • 防止使用脚本大量注册、请求等 。比如发送请求时,判断用户在某个页面停留了多长时间。登录、注册时是否点击了登录、注册按钮,如果没有点击就直接发送登录、注册请求,那么这个行为十有八九是脚本、机器行为。

  • 滑块验证有几个重要的数据

    1. 滑块的起点
    2. 滑块的终点
    3. 滑块从起点滑动到终点所用的时间,比如人为滑动长度为240px的滑块,至少需要50毫秒,才能冲起点滑倒终点。若起点滑到终点所用的时间是50毫秒以下,那么这种行为99%是机器、脚本行为。人为滑的没那么快。
    4. 滑块的长度,滑块的长度越长越好,因为长度越长,我们采集到的数据就越多,数据越多就越容易判断是人为还是机器、脚本行为。滑块最好在200px+以上。
    5. 滑动滑块的轨迹,例如滑动一个长度为300px的滑块,至少可以采集到20个点位,每个点位有用户滑动的x,y轴位置、滑动时间等。

验证方式

人为滑动轨迹参考

当我们用手滑动验证滑块时,滑动轨迹为变速曲线运动,上下起伏分布不均、且变速运动。如下图3图

图1

图2

图3

人为/人机/脚本滑动判断

  1. 滑动滑块所用时间低于50毫秒判断为人机/脚本,200px的滑块,人为滑动没那么快。
  2. 判断滑动的线条起伏,若是纯直线,则为人机,因为人为滑动没那么直。
  3. 计算滑动速度,人为滑动肯定是变速滑动,若是匀速滑动则为人机。

若不是人为滑动,多数是直线匀速滑动,不会上下起伏。例如脚本滑动。
但部分机器/脚本可以模拟人为滑动,所以很轻易破解滑块验证。

  • 下面是一串人为滑动的数据,用canvs渲染出来,

    • x - x轴位置,
    • y - y轴位置 ,
    • moveTime - 滑动到该点的时间
 <canvas id="cvs" width="300" height="60"></canvas>
<script>
const draw = data => { const cvs = document.querySelector('#cvs'),c = cvs.getContext('2d')c.clearRect(0, 0, cvs.width, cvs.height)c.lineWidth = 2c.strokeStyle = 'red'c.beginPath()data.forEach((it, i) => {if (i === 0) c.moveTo(it.x, it.y)c.lineTo(it.x, it.y)})c.stroke()
}const data = [{"x":41.4,"y":24.05,"moveTime":1684666751003},{"x":45.12,"y":23.12,"moveTime":1684666751014},{"x":47.91,"y":23.12,"moveTime":1684666751019},{"x":52.56,"y":22.19,"moveTime":1684666751027},{"x":56.28,"y":21.26,"moveTime":1684666751035},{"x":60.93,"y":20.33,"moveTime":1684666751043},{"x":63.72,"y":20.33,"moveTime":1684666751051},{"x":68.37,"y":20.33,"moveTime":1684666751059},{"x":73.02,"y":20.33,"moveTime":1684666751067},{"x":77.67,"y":20.33,"moveTime":1684666751075},{"x":83.26,"y":20.33,"moveTime":1684666751083},{"x":87.91,"y":20.33,"moveTime":1684666751091},{"x":93.49,"y":20.33,"moveTime":1684666751099},{"x":101.86,"y":20.33,"moveTime":1684666751107},{"x":109.3,"y":20.33,"moveTime":1684666751115},{"x":117.67,"y":20.33,"moveTime":1684666751124},{"x":125.12,"y":20.33,"moveTime":1684666751131},{"x":131.63,"y":20.33,"moveTime":1684666751139},{"x":139.07,"y":20.33,"moveTime":1684666751148},{"x":145.58,"y":21.26,"moveTime":1684666751155},{"x":151.16,"y":22.19,"moveTime":1684666751163},{"x":155.81,"y":23.12,"moveTime":1684666751171},{"x":161.4,"y":24.05,"moveTime":1684666751179},{"x":166.05,"y":24.05,"moveTime":1684666751187},{"x":170.7,"y":24.05,"moveTime":1684666751195},{"x":177.21,"y":24.05,"moveTime":1684666751204},{"x":181.86,"y":24.05,"moveTime":1684666751211},{"x":186.51,"y":24.05,"moveTime":1684666751219},{"x":191.16,"y":24.05,"moveTime":1684666751227},{"x":195.81,"y":24.05,"moveTime":1684666751236},{"x":201.4,"y":24.05,"moveTime":1684666751243},{"x":206.98,"y":24.05,"moveTime":1684666751251},{"x":213.49,"y":24.05,"moveTime":1684666751259},{"x":220,"y":24.05,"moveTime":1684666751268},{"x":227.44,"y":24.05,"moveTime":1684666751275},{"x":236.74,"y":24.05,"moveTime":1684666751283},{"x":245.12,"y":24.05,"moveTime":1684666751291},{"x":254.42,"y":24.05,"moveTime":1684666751299},{"x":261.86,"y":24.05,"moveTime":1684666751307},{"x":268.37,"y":24.05,"moveTime":1684666751315},{"x":273.02,"y":24.05,"moveTime":1684666751323},{"x":279.53,"y":24.05,"moveTime":1684666751332}]draw(data )
</script>

渲染图

滑动验证 (vue3)

Props

props 类型 默认值 作用
width Number 300 滑块宽度
height Number 45 滑块高度
servertest Blooear false 是否开启后后端验证 - 点我跳转详情
drop-color String #fff 滑块颜色
tip-none-color String #000 待验证文本色
tip-suc-color String #fff 验证成功文本色
tip-test-ing-color String #fff 验证中的文本色
tip-tail-color String #ee0a24 验证失败文本色
slide-color String #ee0a24 滑块背景色颜色
success-bg-color String #07c160 验证通过背景色
tail-bg-color String #ee0a24 验证失败背景色
active-bg-color String #1989fa 已激活的背景色
test-ing-bg-color String #ff976a 验证中激活的背景色
font-size Number 16 文本大小
test-tip String 正在验证… 验证中提示文本
tip-txt String 向右滑动验证 待验证提示文本
success-tip String 太棒了,恭喜你验证通过! 验证成功文字提示
fail-tip String 验证失败,请重试 验证失败文字提示

事件

事件 作用 参数 备注
statu 验证状态 (vfcStatu, slideInfo ) vfcStatu.statu→验证状态 。slideInfo →滑动轨迹信息 vfcStatu.statu有4种状态 。①tail - 验证失败 ②success - 验证成功 ③testing - 前端验证中 ④ servertest - 后端验证中,props的servertest设置未true才会触发后端验证(若servertest为true,不设置vfcStatu.statu状态会一直处于验证中动画

vue验证组件

SliderVfc.vue

<template><canvas :class="cvsClass" :width="props.width" :height="props.height" ref="cvs"></canvas>
</template><script setup>
const props = defineProps({// 是否开启服务端验证 servertest: {type: Boolean,default: false},width: {type: Number,default: 300},height: {type: Number,default: 45},strokeWidth: {type: Number,default: 5},// 滑块宽度dropWidth: {type: Number,default: 50},// 已激活验证背景色 activeBgColor | 验证中激活的背景色 testIngBgColor| 验证成功激活的背景色 successBgColor// 验证成功文本色 tipSucColor| 验证失败文本色 tipTailColor | 验证中的文本色 tipTestIngColor | 待验证文本色 tipNoneColor// 移动滑块背景色 dropColor// 滑块原始背景色  slideColor// 滑块颜色dropColor: {type: String,default: '#fff'},// 待验证文本色tipNoneColor: {type: String,default: '#000'},// 验证成功文本色tipSucColor: {type: String,default: '#fff'},// 验证中的文本色tipTestIngColor: {type: String,default: '#fff'},// 验证失败文本色tipTailColor: {type: String,default: '#ee0a24'},// 验证中提示testTip: {type: String,default: '正在验证...'},// 滑块背景色颜色slideColor: {type: String,default: '#e8e8e8'},// 滑块背景色颜色tipTxt: {type: String,default: '向右滑动验证'},// 验证通过背景色successBgColor: {type: String,default: '#07c160'},//  验证失败背景色tailBgColor: {type: String,default: '#ee0a24'},// 已激活的背景色activeBgColor: {type: String,default: '#1989fa'},// 验证中激活的背景色testIngBgColor: {type: String,default: '#ff976a'},// 验证成功文字提示successTip: {type: String,default: '太棒了,恭喜你验证通过!'},// 验证失败文字提示failTip: {type: String,default: '验证失败,请重试'},// 文本大小fontSize: {type: Number,default: 16},})
const emit = defineEmits(['statu'])let vfcx = null
const cvs = ref()
const cvsClass = ref('cur-none')let vfcres = {startX: 0,//开始拖动滑块位置endX: 0,//结束拖动滑块位置 timed: 0,//拖动所用时间 || 低于30毫秒认定为机器guiji: [],//拖动轨迹 | 连续2个2数之差相同判定为机器  width: props.width
}const vfcStatu = reactive({statu: 'none'
})
// 监听数据,并发给父级
watch(vfcStatu, res => {emit('statu', res, vfcres)// 验证成功if (res.statu === 'success') {vfcx.anmateOff = falsevfcx.activeBgColor = props.successBgColorvfcx.tipTxt = props.successTipvfcx.colors.slideColor = props.successBgColorvfcx.evNone()} else if (res.statu === 'tail') {vfcx.reset()vfcx.tipTxt = props.failTipvfcx.fontColor = props.tipTailColorvfcx.draw()}
})/**
* 验证器
* @param {Element} cvsEl canvas元素
* @param {String, default:'cur-none'} cvsClass canvas的class
* @param {Boolear, default:fasle} vfcres 验证结果
* @param {Number, default:5} strokeWidth 滑块内边距
* @param {Number,default:50} dropWidth 滑块宽度
* @param {color,default:'#fff'} dropColor 移动滑块背景色
* @param {color,default:'#e8e8e8'} slideColor 滑块背景色颜色
* @param {color,default:'skyblue'} activeBgColor 已激活验证背景色
* @param {color,default:'#ff976a'} testIngBgColor 验证中激活的背景色
* @param {color,default:'#07c160'} successBgColor 验证成功激活的背景色
* @param {color,default:'#07c160'} tipSucColor 验证成功文本色
* @param {color,default:'#ee0a24'} tipTailColor 验证失败文本色
* @param {color,default:'#fff'} tipTestIngColor 验证中的文本色
* @param {color,default:'#000'} tipNoneColor 待验证文本色
* @param {String,default:'向右滑动验证'} tipTxt 文字提示
* @param {String,default:'太棒了,恭喜你验证通过!'} successTip 验证成功文字提示
* @param {String,default:'验证失败,请重试...'} failTip 验证失败文字提示
* @param {Bool} servertest 是否开启前端验证模式
* @param {String} testTip 验证提示
*/
class Vfcs {constructor(cvsEl, cvsClass, vfcres, vfcStatu, strokeWidth, dropWidth, fontSize, servertest, colors, tipTxt) {this.cvsEl = cvsElthis.vfcres = vfcresthis.cvsClass = cvsClassthis.strokeWidth = strokeWidththis.dropWidth = dropWidththis.vfcStatu = vfcStatuthis.colors = colorsthis.fontSize = fontSizethis.dwonIsPath = false //是否按下验证滑块this.ctx = nullthis.allTipTxts = tipTxtthis.tipTxt = this.allTipTxts.tipTxtthis.fontColor = this.colors.tipNoneColorthis.activeBgColor = this.colors.activeBgColorthis.servertest = servertestthis.guiji = []this.startTime = 0this.endTime = 0this.startX = 0this.startY = 0this.moveX = 0this.moveY = 0this.fontOp = 1  //文本透明度this.met = falsethis.offX = 0//x轴的位移this.minX = this.strokeWidth / 2this.maxX = this.cvsEl.width - this.dropWidth - this.strokeWidth// this.dropX最大值 -》  cW - this.dropWidth - this.strokeWidth / 2// this.dropX最小 -》   this.strokeWidth / 2this.dropX = this.minX + this.offX // 滑块位置this.toTouchEnd = false//是否按下滑块this.isDown = falsethis.testAm = null //验证中动画的id this.anmateOff = true//动画开关 this.evsName = []//事件名 this.evsFun = [this.down.bind(this), this.move.bind(this), this.up.bind(this)]//事件方法    this.init()}init() {this.ctx = this.cvsEl.getContext('2d')this.draw()this.evsName = this.evType()// 给canvas添加事件  this.evsName.forEach((evName, i) => i === 0 ? this.cvsEl.addEventListener(evName, this.evsFun[i]) : document.addEventListener(evName, this.evsFun[i]))}// 绘制draw() {let cW = this.cvsEl.width,cH = this.cvsEl.height,c = this.ctxc.clearRect(0, 0, cW, cH)c.globalAlpha = this.fontOp // 设置图像透明度 c.fillRect(0, 0, cW, cH)c.fillStyle = this.colors.slideColorc.strokeStyle = this.colors.slideColorc.lineWidth = this.strokeWidthc.fillRect(0, 0, cW, cH)c.strokeRect(0, 0, cW, cH)// 激活背景色c.fillStyle = this.activeBgColorc.strokeStyle = this.activeBgColorc.fillRect(this.minX + 2, this.minX, this.offX, cH - this.strokeWidth)// 文本提示c.textAlign = "center"c.textBaseline = 'middle'c.fillStyle = this.fontColorc.font = `${this.fontSize}px 黑体`c.fillText(this.tipTxt, cW / 2, cH / 2)// 验证失败  // 待验证 | 验证中if (this.vfcStatu.statu === 'none' || this.vfcStatu.statu === 'testing' || this.vfcStatu.statu === 'servertest' || this.vfcStatu.statu === 'tail') {// 滑块 c.beginPath()c.fillStyle = this.colors.dropColorc.rect(this.dropX, this.minX, this.dropWidth, cH - this.strokeWidth)c.fill()// 箭头  c.lineWidth = 2// 右边箭头c.moveTo(this.dropX + this.dropWidth / 1.7 - 5, this.strokeWidth + 10)c.lineTo(this.dropX + this.dropWidth / 1.7 + 5, cH / 2)c.lineTo(this.dropX + this.dropWidth / 1.7 - 5, cH - this.strokeWidth - 10)// 左边箭头c.moveTo(this.dropX + this.dropWidth / 1.7 - 15, this.strokeWidth + 10)c.lineTo(this.dropX + this.dropWidth / 1.7 - 5, cH / 2)c.lineTo(this.dropX + this.dropWidth / 1.7 - 15, cH - this.strokeWidth - 10)c.stroke()c.closePath()// 验证成功} else if (this.vfcStatu.statu === 'success') {// 滑块 c.beginPath()c.fillStyle = this.colors.dropColorc.rect(this.dropX, this.minX, this.dropWidth, cH - this.strokeWidth)c.fill()c.closePath()// 圈c.beginPath()c.fillStyle = this.colors.successBgColorc.arc(this.dropWidth / 2 + this.dropX, cH / 2, cH / 3, 0, 2 * Math.PI)c.fill()c.closePath()// 勾c.beginPath()c.lineWidth = 3c.lineJoin = "bevel"c.lineCap = "round"c.strokeStyle = this.colors.dropColorc.moveTo(this.dropX + this.dropWidth / 2 - 8, cH / 2 + 1)c.lineTo(this.dropX + this.dropWidth / 2.1, cH / 1.6)c.lineTo(this.dropX + this.dropWidth / 2 + 8, cH / 2 - 5)c.stroke()c.closePath()}}// 滑块按下down(ev) {if (this.vfcStatu.statu === 'testing' || this.vfcStatu.statu === 'servertest') returnthis.setXY(ev)//按下滑块this.isDown = truethis.startTime = new Date().getTime()// 若按下滑块const isPath = this.ctx.isPointInPath(this.startX, this.startY)this.dwonIsPath = isPath}// 滑块移动move(ev) {if (this.vfcStatu.statu === 'testing' || this.vfcStatu.statu === 'servertest') returnthis.setXY(ev)const isPath = this.ctx.isPointInPath(this.moveX, this.moveX)// pc 鼠标变手势if (ev.x) isPath === true ? this.cvsClass.value = 'cur' : this.cvsClass.value = 'cur-none'const x = Number(this.moveX.toFixed(2))const y = Number(this.moveY.toFixed(2))const moveTime = new Date().getTime()this.guiji.push({ x, y, moveTime })if (this.dwonIsPath === false || this.moveX <= 0) returnif (this.isDown === true) {// 若滑到尾部 this.toTouchEnd = this.touchDrosToEnd()if (this.toTouchEnd === true) this.up()this.draw()}}// 滑块抬起up() {if (this.vfcStatu.statu === 'testing' || this.vfcStatu.statu === 'servertest' || this.offX === 0 || this.dwonIsPath === false || this.moveX <= 0) returnthis.endTime = new Date().getTime()this.vfcres.startX = this.startX//鼠标/手指按下位置this.vfcres.endX = this.dropX + this.dropWidth + this.minX//鼠标/手指抬起位置 this.vfcres.timed = this.endTime - this.startTime//耗时this.vfcres.guiji = this.guiji//滑动轨迹   this.vfcres.width = this.cvsEl.widththis.dwonIsPath = falsethis.isDown = false// 未滑动到尾部if (this.toTouchEnd === false) {this.dropX = this.minX// 滑块位置this.offX = 0this.tipTxt = this.allTipTxts.failTipthis.fontColor = this.colors.tipTailColor// 滑动到尾部} else {this.vfcStatu.statu = 'testing'this.testAdmate() //开启动画// 验证中   this.fontColor = this.colors.tipTestIngColorthis.tipTxt = this.allTipTxts.testTipthis.activeBgColor = this.colors.testIngBgColorthis.dropX = this.maxX + this.minX// 滑块位置  const test = this.testVer()setTimeout(() => {// 前端验证通过if (test === 'success') {// 已开启前端验证模式 if (this.servertest === true) {this.vfcStatu.statu = 'servertest'} else {this.vfcStatu.statu = 'success'}// 前端验证不通过} else {this.vfcStatu.statu = 'tail'}}, 1000)}this.draw()this.guiji = []}// 重置滑块reset() {this.dropX = this.minX// 滑块位置this.anmateOff = falsethis.activeBgColor = this.colors.activeBgColorthis.fontColor = this.colors.tipNoneColorthis.tipTxt = this.allTipTxts.tipTxtthis.offX = 0this.toTouchEnd = falsethis.guiji = []this.draw()}// 解绑事件    evNone() {this.evsName.forEach((evName, i) => i === 0 ? this.cvsEl.removeEventListener(evName, this.evsFun[i]) : document.removeEventListener(evName, this.evsFun[i]))}// 验证中动画testAdmate() {// 文本透明度if (this.met === false && this.fontOp >= 1) {this.met = true} else if (this.met === true && this.fontOp <= .5) {this.met = false}this.met === false ? this.fontOp += .015 : this.fontOp -= .015this.draw()cancelAnimationFrame(this.testAm)this.testAm = window.requestAnimationFrame(this.testAdmate.bind(this))if (this.anmateOff === false) {cancelAnimationFrame(this.testAm)this.fontOp = 1this.testAm = nullthis.met = falsethis.anmateOff = true}this.draw()}/*** 验证是否滑动到尾部* @return {Number}  return true 到尾部,false 没到尾部*/touchDrosToEnd() {const x = this.offX + this.dropWidth + this.strokeWidthconst isSuccess = x >= this.cvsEl.widthreturn isSuccess}// 设置xy坐标  setXY(ev) {if (ev.type === 'touchstart') {this.startX = ev.touches[0].clientX - this.cvsEl.getBoundingClientRect().leftthis.startY = ev.touches[0].clientY - this.cvsEl.getBoundingClientRect().top}if (ev.type === 'touchmove') {this.moveX = ev.touches[0].clientX - this.cvsEl.getBoundingClientRect().leftthis.moveY = ev.touches[0].clientY - this.cvsEl.getBoundingClientRect().top}// ///pc事件 //if (ev.type === 'mousedown') {this.startX = ev.x - this.cvsEl.getBoundingClientRect().leftthis.startY = ev.y - this.cvsEl.getBoundingClientRect().top}if (ev.type === 'mousemove') {this.moveX = ev.x - this.cvsEl.getBoundingClientRect().leftthis.moveY = ev.y - this.cvsEl.getBoundingClientRect().top}// 防止滑块溢出指定范围if (ev.type === 'mousemove' || ev.type === 'touchmove') {this.offX = this.moveX - this.startXif (this.offX > this.maxX) this.offX = this.maxXif (this.offX < this.minX) this.offX = this.minXthis.dropX = this.minX + this.offX // 滑块位置}}// 事件类型evType() {const isMobile =navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i) !== nullconst events = isMobile? ['touchstart', 'touchmove', 'touchend']: ['mousedown', 'mousemove', 'mouseup']return events}/*** 滑动轨迹信息 | 计算滑动轨迹每2数之间的差值 | 出现次数等* @return {Object(chaArr,repeatX,repeatY,repeatMaxXCount,repeatMaxYCount,allCount)} chaArr → 每2数之间的插值 | repeatX → x轴每2数之间的差值与重复数  | repeatY → y轴每2数之间的差值与重复数 |  repeatMaxXCount → x轴每重复数最多的次数 | repeatMaxYCount → y轴每重复数最多的次数 */arrCmp() {// 重复的数量const repeatX = []const repeatY = []const timed = []const chaArr = this.guiji.reduce((prev, itm, i, arr) => {if (i === arr.length - 1) return prevconst nv = arr[i + 1]const chaX = Number((nv.x - itm.x).toFixed(2))const chaY = Number((nv.y - itm.y).toFixed(2))const timeCha = nv.moveTime - itm.moveTimetimed.push(timeCha)//时间差// 是否有重复的数组const repeatXIndex = repeatX.findIndex(item => item.num === chaX)const repeatYIndex = repeatY.findIndex(item => item.num === chaY)// xy轴每2数差数据if (repeatXIndex === -1) {const obj = {num: chaX,count: 1}repeatX.push(obj)} else {repeatX[repeatXIndex].count++}if (repeatYIndex === -1) {const obj = {num: chaY,count: 1}repeatY.push(obj)} else {repeatY[repeatYIndex].count++}prev.push({ x: chaX, y: chaY })return prev}, [])// 所有重复次数const findXCount = []const findYCount = []repeatX.forEach(it => findXCount.push(it.count))repeatY.forEach(it => findYCount.push(it.count))const repeatMaxXCount = Math.max(...findXCount)//x重复最多的次数const repeatMaxYCount = Math.max(...findYCount)//y重复最多的次数const repeatMaxTimed = Math.max(...timed)//滑动时间重复最多的次数return {chaArr,repeatX,repeatY,repeatMaxXCount,repeatMaxYCount,repeatMaxTimed}}// 前端验证//  x轴最大波动大于数等于所有波动长度则为人机 | y轴最大波动数等于所有波动长度则为人机 | 滑动时间低于50毫秒不通过  | 时间波动最大次数大于滑动轨迹长度的3/1为人机testVer() {// return 'tail'// 滑动所用时间低于50毫秒 是人机if (this.vfcres.timed < 50) return 'tail'const sliderInfo = this.arrCmp()//处理滑动轨迹信息    // 时间波动最大次数等于sliderInfo.chaArr.length滑动轨迹长度为人机const timeTest = sliderInfo.repeatMaxTimed === sliderInfo.chaArr.lengthif (timeTest === true) return 'tail'// x轴最大波动大于数等于所有波动长度则为人机if (sliderInfo.repeatMaxXCount === sliderInfo.repeatX) return 'tail'//  y轴最大波动数等于所有波动长度则为人机if (sliderInfo.repeatMaxYCount === sliderInfo.chaArr.length) return 'tail'// 是真人return 'success'}
}
nextTick(() => {const colors = {activeBgColor: props.activeBgColor,testIngBgColor: props.testIngBgColor,successBgColor: props.successBgColor,tipSucColor: props.tipSucColor,tipTailColor: props.tipTailColor,tipTestIngColor: props.tipTestIngColor,tipNoneColor: props.tipNoneColor,dropColor: props.dropColor,slideColor: props.slideColor,}const tipTxt = {testTip: props.testTip,tipTxt: props.tipTxt,successTip: props.successTip,failTip: props.failTip,}vfcx = new Vfcs(cvs.value,cvsClass,vfcres,vfcStatu,props.strokeWidth,props.dropWidth,props.fontSize,props.servertest,colors,tipTxt)
})
</script>
<style scoped>
.cur {cursor: pointer;
}.cur-none {cursor: default;
}
</style>

使用滑动验证

Home.vue

 <slider-vfc  @statu="slide" />
<script setup>
// 滑块验证
const slide = (vfcStatu, slideInfo) => {/**
这里可以做一些自定义验证
- vfcStatu.statu有2状态,必须赋值状态
- success 验证成功状态
- tail 验证失败状态
- 可配合后端验证
*/const statu = vfcStatu.statu  if (statu) {if (statu === 'success') {console.log('验证成功')} else if (statu === 'tail') {console.log('验证失败')}}
}
</script>

成功效果图

失败效果图

验证中效果图

验证中会有一个透明度效果

搭配后端验证(axios | express

  • 搭配后端验证,主要用到axios 与 express。后端验证的原理与前端验证是一个原理,只不过换了给地方做验证。
  • 但是还是不安全,因为前端可以伪造数据,然后给后端发送请求,后端返回数据。
  • 纯滑块还是要搭配检测行为才好使,因为滑块只是访问某个页面或请求某个数据时的多环验证的其中一环而已,比如用户停留某个页面多长时间,各方面因素来判断当前行为是否是人为、还是脚本\机器行为等等。

后端代码

import express from 'express'const app = express()
const router = express.Router()app.use(router)/**
* 滑块验证
* @data {Array} 前端传送data 验证的数据
* @res  {String(success,tail)}  success 验证成功 | tail验证失败
*/
router.post('/api/slidetest', (req, res) => {const qy = req.query// 重复的数量const repeatX = [],repeatY = [],timed = []const data = JSON.parse(qy.datainfo)let resInfo = ''const chaArr = data.guiji.reduce((prev, itm, i, arr) => {if (i === arr.length - 1) return prevconst nv = arr[i + 1]const chaX = Number((nv.x - itm.x).toFixed(2))const chaY = Number((nv.y - itm.y).toFixed(2))const timeCha = nv.moveTime - itm.moveTimetimed.push(timeCha)//时间差// 是否有重复的数组const repeatXIndex = repeatX.findIndex(item => item.num === chaX)const repeatYIndex = repeatY.findIndex(item => item.num === chaY)// xy轴每2数差数据if (repeatXIndex === -1) {const obj = {num: chaX,count: 1}repeatX.push(obj)} else {repeatX[repeatXIndex].count++}if (repeatYIndex === -1) {const obj = {num: chaY,count: 1}repeatY.push(obj)} else {repeatY[repeatYIndex].count++}prev.push({ x: chaX, y: chaY })return prev}, [])// 所有重复次数const findXCount = []const findYCount = []repeatX.forEach(it => findXCount.push(it.count))repeatY.forEach(it => findYCount.push(it.count))const repeatMaxXCount = Math.max(...findXCount)//x重复最多的次数const repeatMaxYCount = Math.max(...findYCount)//y重复最多的次数const repeatMaxTimed = Math.max(...timed)//滑动时间重复最多的次数// 时间波动最大次数等于chaArr.length滑动轨迹长度为人机const timeTest = repeatMaxTimed === chaArr.length// 滑动所用时间低于50毫秒 是人机// x轴最大波动大于数等于所有波动长度则为人机// y轴最大波动数等于所有波动长度则为人机if (data.timed < 50 || timeTest === true || repeatMaxXCount === repeatX || repeatMaxYCount === chaArr.length) {resInfo = 'tail'} else {// 是真人resInfo = 'success'}
// console.log(resInfo);res.end(resInfo)
})const host = '127.0.0.1'
const port = 3456
app.listen(port, host, () => {console.log(host + ':' + port + '/api/slidetest')
})

例:前端登录代码

需求:

  1. 用户登录输入的用户名与密码合法
  2. 用户点击登录按钮,不直接向后端发起登录请求,而是弹出验证滑块
  3. 验证成功 → 发起登录请求
  4. 验证成功 → 提示验证失败 ,不发起登录请求

login.vue

<template><div style="display: flex; flex-direction: column;"><input type="text" name="userName" placeholder="用户名" v-model="userInfo.userName"><input type="password" name="pwd" placeholder="密码" v-model="userInfo.pwd"><input type="button" value="登录" @click="login"></div><slider-vfc v-if="userInfo.showVfc" servertest @slide="login" style="margin: 1em 0 0 0;" />
</template><script setup>
import axios from 'axios'
import { reactive } from 'vue'const userInfo = reactive({userName: '',pwd: '',showVfc: false,
})const login = (vfcStatu, slideInfo) => {// 进行简单校验 。仅作测试,真实开发中需严格校验if (userInfo.userName.length >= 3 && userInfo.pwd.length >= 3) {userInfo.showVfc = true} else {alert('请输入合法信息')return}if (vfcStatu.statu) {axios.post('/api/slidetest?datainfo=' + JSON.stringify(slideInfo)).then(res => {// 若滑块验证成功  验证成功可以做一些登录。注册等请求vfcStatu.statu = res.dataif (res.data === 'success') {console.log('验证成功')   console.log('这里可以做一些登录请求')axios.post(`/api/login?unm=${userInfo.userName}&pwd=${userInfo.pwd}`)} else if (res.data === 'tail') {console.log('验证失败')} }).catch(err => {// 若滑块验证出错vfcStatu.statu = 'tail'})}
}
</script>

输入前

输入的信息合法 + 点击登录按钮后

验证成功发起登录请求

验证失败

成品展示

登录前

未输入登录信息登录时

输入的信息不合法时

  • 输入信息不合法不应该弹出滑块验证

输入信息合法时

验证成功时

验证成功自动登录,反之,验证不成功不会发起登录请求,并提示重验

【Vue3】滑动验证组件 | 滑动验证相关推荐

  1. 【MSDN文摘】使用自定义验证组件库扩展 Windows 窗体: Form Scope

    使用自定义验证组件库扩展 Windows 窗体,第 2 部分(Windows 窗体探索) 发布日期: 5/28/2004 | 更新日期: 5/28/2004 Michael Weinhardt www ...

  2. react native ScrollView实现滑动锚定,滑动到指定位置

    实现ScrollView滑动视图组件滑动到指定位置,实现tab与具体位置相锚定 给需要锚定的组件加上onLayout属性 //event.nativeEvent.layout.x是水平方向值,even ...

  3. vue3.2+ 滑动验证组件,pc/手机通用,即插即用

    vue3.2+ 滑动验证组件,pc/手机通用,即插即用 一.前言 二.成果展示 三.组件使用 四.vue3.2+ 滑动验证组件 源码 五.最后,点个赞 一.前言 vue已经更新到3.2+,使用了scr ...

  4. vue3 滑动验证组件

    vue3拖动滑块进行验证 组件效果展示 源码 <template><div class="slider-verification" ref="slide ...

  5. wordpress滑动验证_WordPress图像滑块:滑动还是不滑动?

    wordpress滑动验证 Image sliders (also known as image carousels or slideshows) can be a convenient way to ...

  6. 前端滑动验证+拼图滑动验证效果

    相信大家都玩过B站,B站在登陆的时候有个拼图滑动验证,今天就整合一下前端实现的滑动验证 拖动滑动验证(无背景图片) <!DOCTYPE html> <html> <hea ...

  7. C#滑动验证码、拼图验证

    在现有的系统中,滑动验证.拼图验证应用日渐频繁,让我们来亲自做一个. 实现分析: 滑动验证码的逻辑也很简单.大概说一下: 1,服务器生成主图+附图(从主图裁剪下来的不需要管y坐标)并且存储X坐标: 2 ...

  8. 微信小程序图片验证组件封装

    一.图片滑动验证组件 延迟页面展示或者延缓并发处理.当滑动图片到空缺位置后执行加载或者验证. 二.封装源码 1.wxml <!--遮罩层,弹框图片背景,滑动框图片比例:16:9 1.777777 ...

  9. Vue基于ElementUI组件实现滑块登录验证组件

    引言 在实际项目应用开发中,为了防止用户频繁发起登录请求,导致后端登录访问压力瞬时过大,我们可以设计一些验证规则防刷,最常见的方式是通过输入验证码的方式降低刷新频率,后台通过返回不同的验证码从而降低用 ...

最新文章

  1. [linux] shell脚本编程-ubuntu创建vsftpd服务
  2. 【最小费用可行流模板】
  3. jQuery的name选择器 模糊匹配
  4. shiro框架,自定义realm注入service失败解决办法
  5. 《网络管理员考试案例导学》复习重点
  6. 驱动兼容_【重磅】上海数明发布国内首款兼容光耦隔离式单通道栅极驱动器系列产品SLM34x...
  7. Echarts数据可视化grid直角坐标系(xAxis、yAxis),开发全解+完美注释
  8. P1603 斯诺登的密码-字符串加法的妙用
  9. FZU 2129 子序列个数(DP)题解
  10. 2017.10.26-构建之法:现代软件工程-阅读笔记
  11. 前端开发_HTML5_布局-div+css布局
  12. 2018年8月12日 今日头条笔试 整理
  13. 还原扩容的缩水U盘真实容量方法,让数据免受损失
  14. amoeba mysql mmm_MMM+Amoeba搭建MySQL高可用负载均衡群集
  15. MySQL用户权限系统
  16. 计算机专业学习规划,计算机专业学习计划.doc
  17. Microbiome | 东北农大石宝明/孟庆维等揭示宿主-微生物互作介导猪肠炎免疫
  18. 小知识(3) 解决谷歌翻译问题(浏览器/IDEA)
  19. 【计算机图形学 】扫描线多边形填充算法 | OpenGL+鼠标交互
  20. 袁萌:Linux病毒为何不会泛滥成灾?

热门文章

  1. 2021-2027全球与中国塑料热成型产品市场现状及未来发展趋势
  2. SPI总线以及驱动详解
  3. 京东物流青龙系统分享
  4. python做一个问答系统_手把手教你用Python搭建一个AI智能问答系统
  5. gitlab拉取、上传指定文件目录
  6. QDate转换为时间戳
  7. 支付宝红包无线支付服务器,网上发红包无线支付成市民新宠 1.5万人次通过支付宝送红包...
  8. 【Unity入门计划】基本概念(6)-精灵渲染器 Sprite Renderer
  9. 安卓手机如何让应用程序静音
  10. 选用MOS管ASE10N65SE-ASEMI应当注意哪几方面