滑块验证不只判断是否滑动到尾部,真正的目的是检测用户行为,检测行为是人为、脚本、还是其它。
防止使用脚本大量注册、请求等 。比如发送请求时,判断用户在某个页面停留了多长时间。登录、注册时是否点击了登录、注册按钮,如果没有点击就直接发送登录、注册请求,那么这个行为十有八九是脚本、机器行为。
滑块验证有几个重要的数据
当我们用手滑动验证滑块时,滑动轨迹为变速曲线运动,上下起伏分布不均、且变速运动。如下图3图
若不是人为滑动,多数是直线匀速滑动,不会上下起伏。例如脚本滑动。
但部分机器/脚本可以模拟人为滑动,所以很轻易破解滑块验证。
<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 = 2
c.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>
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状态会一直处于验证中动画) |
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 = false
vfcx.activeBgColor = props.successBgColor
vfcx.tipTxt = props.successTip
vfcx.colors.slideColor = props.successBgColor
vfcx.evNone()
} else if (res.statu === 'tail') {
vfcx.reset()
vfcx.tipTxt = props.failTip
vfcx.fontColor = props.tipTailColor
vfcx.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 = cvsEl
this.vfcres = vfcres
this.cvsClass = cvsClass
this.strokeWidth = strokeWidth
this.dropWidth = dropWidth
this.vfcStatu = vfcStatu
this.colors = colors
this.fontSize = fontSize
this.dwonIsPath = false //是否按下验证滑块
this.ctx = null
this.allTipTxts = tipTxt
this.tipTxt = this.allTipTxts.tipTxt
this.fontColor = this.colors.tipNoneColor
this.activeBgColor = this.colors.activeBgColor
this.servertest = servertest
this.guiji = []
this.startTime = 0
this.endTime = 0
this.startX = 0
this.startY = 0
this.moveX = 0
this.moveY = 0
this.fontOp = 1 //文本透明度
this.met = false
this.offX = 0//x轴的位移
this.minX = this.strokeWidth / 2
this.maxX = this.cvsEl.width - this.dropWidth - this.strokeWidth
// this.dropX最大值 -》 cW - this.dropWidth - this.strokeWidth / 2
// this.dropX最小 -》 this.strokeWidth / 2
this.dropX = this.minX + this.offX // 滑块位置
this.toTouchEnd = false
//是否按下滑块
this.isDown = false
this.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.ctx
c.clearRect(0, 0, cW, cH)
c.globalAlpha = this.fontOp // 设置图像透明度
c.fillRect(0, 0, cW, cH)
c.fillStyle = this.colors.slideColor
c.strokeStyle = this.colors.slideColor
c.lineWidth = this.strokeWidth
c.fillRect(0, 0, cW, cH)
c.strokeRect(0, 0, cW, cH)
// 激活背景色
c.fillStyle = this.activeBgColor
c.strokeStyle = this.activeBgColor
c.fillRect(this.minX + 2, this.minX, this.offX, cH - this.strokeWidth)
// 文本提示
c.textAlign = "center"
c.textBaseline = 'middle'
c.fillStyle = this.fontColor
c.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.dropColor
c.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.dropColor
c.rect(this.dropX, this.minX, this.dropWidth, cH - this.strokeWidth)
c.fill()
c.closePath()
// 圈
c.beginPath()
c.fillStyle = this.colors.successBgColor
c.arc(this.dropWidth / 2 + this.dropX, cH / 2, cH / 3, 0, 2 * Math.PI)
c.fill()
c.closePath()
// 勾
c.beginPath()
c.lineWidth = 3
c.lineJoin = "bevel"
c.lineCap = "round"
c.strokeStyle = this.colors.dropColor
c.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') return
this.setXY(ev)
//按下滑块
this.isDown = true
this.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') return
this.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) return
if (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) return
this.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.width
this.dwonIsPath = false
this.isDown = false
// 未滑动到尾部
if (this.toTouchEnd === false) {
this.dropX = this.minX// 滑块位置
this.offX = 0
this.tipTxt = this.allTipTxts.failTip
this.fontColor = this.colors.tipTailColor
// 滑动到尾部
} else {
this.vfcStatu.statu = 'testing'
this.testAdmate() //开启动画
// 验证中
this.fontColor = this.colors.tipTestIngColor
this.tipTxt = this.allTipTxts.testTip
this.activeBgColor = this.colors.testIngBgColor
this.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 = false
this.activeBgColor = this.colors.activeBgColor
this.fontColor = this.colors.tipNoneColor
this.tipTxt = this.allTipTxts.tipTxt
this.offX = 0
this.toTouchEnd = false
this.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 -= .015
this.draw()
cancelAnimationFrame(this.testAm)
this.testAm = window.requestAnimationFrame(this.testAdmate.bind(this))
if (this.anmateOff === false) {
cancelAnimationFrame(this.testAm)
this.fontOp = 1
this.testAm = null
this.met = false
this.anmateOff = true
}
this.draw()
}
/**
* 验证是否滑动到尾部
* @return {Number} return true 到尾部,false 没到尾部
*/
touchDrosToEnd() {
const x = this.offX + this.dropWidth + this.strokeWidth
const isSuccess = x >= this.cvsEl.width
return isSuccess
}
// 设置xy坐标
setXY(ev) {
if (ev.type === 'touchstart') {
this.startX = ev.touches[0].clientX - this.cvsEl.getBoundingClientRect().left
this.startY = ev.touches[0].clientY - this.cvsEl.getBoundingClientRect().top
}
if (ev.type === 'touchmove') {
this.moveX = ev.touches[0].clientX - this.cvsEl.getBoundingClientRect().left
this.moveY = ev.touches[0].clientY - this.cvsEl.getBoundingClientRect().top
}
// ///pc事件 //
if (ev.type === 'mousedown') {
this.startX = ev.x - this.cvsEl.getBoundingClientRect().left
this.startY = ev.y - this.cvsEl.getBoundingClientRect().top
}
if (ev.type === 'mousemove') {
this.moveX = ev.x - this.cvsEl.getBoundingClientRect().left
this.moveY = ev.y - this.cvsEl.getBoundingClientRect().top
}
// 防止滑块溢出指定范围
if (ev.type === 'mousemove' || ev.type === 'touchmove') {
this.offX = this.moveX - this.startX
if (this.offX > this.maxX) this.offX = this.maxX
if (this.offX < this.minX) this.offX = this.minX
this.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
) !== null
const 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 prev
const 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.moveTime
timed.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.length
if (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>
后端代码
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 prev
const 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.moveTime
timed.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')
})
需求:
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.data
if (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>