【Vue3】滑动验证组件 | 滑动验证

news2024/10/7 6:38:50

前言

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

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

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

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

前端验证方式(bwrs)

人为滑动轨迹参考

当我们用手滑动验证滑块时,滑动轨迹为变速曲线运动,上下起伏分布不均、且变速运动。如下图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 = 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>

渲染图
在这里插入图片描述

滑动验证 (vue3)

Props

props类型默认值作用
widthNumber300滑块宽度
heightNumber45滑块高度
bwrsBlooearfalse是否在前端开启验证模式。开启后前端验证通过后再进行自定义验证 - 点我跳转详情
drop-colorString#fff滑块颜色
tip-none-colorString#000待验证文本色
tip-suc-colorString#fff验证成功文本色
tip-test-ing-colorString#fff验证中的文本色
tip-tail-colorString#ee0a24验证失败文本色
slide-colorString#ee0a24滑块背景色颜色
success-bg-colorString#07c160验证通过背景色
tail-bg-colorString#ee0a24验证失败背景色
active-bg-colorString#1989fa已激活的背景色
test-ing-bg-colorString#ff976a验证中激活的背景色
font-sizeNumber16文本大小
test-tipString正在验证…验证中提示文本
tip-txtString向右滑动验证待验证提示文本
success-tipString太棒了,恭喜你验证通过!验证成功文字提示
fail-tipString验证失败,请重试验证失败文字提示

事件

事件名参数作用备注
slide正在验证中事件vfcStatu, slideInfo .vfcStatu.statu→验证状态 。slideInfo →滑动轨迹信息必须设置vfcStatu.statu状态,vfcStatu.statu状态有2值。①tail - 验证失败 ②success - 验证成功 (若不设置状态会一直处于验证中动画

vue代码

SliderVfc.vue

<template>
    <canvas :class="cvsClass" :width="props.width" :height="props.height" ref="cvs"></canvas>
</template>

<script setup>
const props = defineProps({
    // 是否开启前端验证模式
    bwrs: {
        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(['slide'])

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 => {
    // 验证成功
    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()
    } else {
        emit('slide', res, vfcres)
    }
})

/**
* 验证器
* @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} bwrs 是否开启前端验证模式
* @param {String} testTip 验证提示  
*/
class Vfcs {
    constructor(cvsEl, cvsClass, vfcres, vfcStatu, strokeWidth, dropWidth, fontSize, bwrs, 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.bwrs = bwrs

        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 === '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') 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') 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.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.testAdmate() //开启动画
            // 验证中   
            this.fontColor = this.colors.tipTestIngColor
            this.tipTxt = this.allTipTxts.testTip
            this.activeBgColor = this.colors.testIngBgColor
            this.dropX = this.maxX + this.minX// 滑块位置  

            // 已开启前端验证模式 
            if (this.bwrs === true) {
                const test = this.testVer()
 
                // 前端验证通过
                if (test === 'success') {
                    this.vfcStatu.statu = 'testing'

                    // 前端验证不通过
                } else {
                    setTimeout(() => {
                        this.anmateOff = false
                        this.dropX = this.minX// 滑块位置
                        this.offX = 0
                        this.tipTxt = this.allTipTxts.failTip
                        this.fontColor = this.colors.tipTailColor
                        this.activeBgColor = this.colors.activeBgColor
                        this.dropColor
                    }, 500)
                }

                // 验证中
            } else {
                this.vfcStatu.statu = 'testing'
            }
        }
        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() {
        // 滑动所用时间低于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.bwrs,
        colors,
        tipTxt
    )
})
</script> 
<style scoped>
.cur {
    cursor: pointer;
}

.cur-none {
    cursor: default;
}
</style>

使用滑动验证

Home.vue

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

成功效果图

在这里插入图片描述

失败效果图

在这里插入图片描述

验证中效果图

验证中会有一个透明度效果
在这里插入图片描述

搭配后端验证(axios | express)

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

后端代码

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')
})

前端代码

<template>
 		 <!-- bwrs @slide="slide" -->
      <slider-vfc bwrs @slide="slide" />
</template>

<script setup> 
import axios from 'axios'

// 滑块验证 
const slide = (vfcStatu, slideInfo) => { 
   setTimeout(() => {
       axios.post('/api/slidetest?datainfo=' + JSON.stringify(slideInfo)).then(res => vfcStatu.statu = res.data).catch(err => vfcStatu.statu = 'tail')
   }, 1000)
} 
</script>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/552286.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Lesson1——数据结构前言

前言&#xff1a; 今天我们正式开始一个新的专栏——初阶数据结构&#xff08;C语言实现&#xff09;&#xff0c;本专栏后续持续更新时间复杂度空间复杂度、顺序表、链表、栈和队列、二叉树、排序等算法的相关知识&#xff0c;欢迎大家互相学习&#xff0c;可以私信互相讨论哦…

一次oracle环境 enq: TX - allocate ITL entry锁问题分析

enq: TX - allocate ITL entry锁问题分析 通过分析问题时间段两个节点的AWR报告&#xff0c;TOP1等待为锁竞争enq: TX - allocate ITL entry&#xff0c;该等待事件是由于缺省情况下创建的表的INITRANS参数为1,索引的INITRANS参数值为2.当有太多的并发DML操作的数据行处于相同的…

日志模块封封装:单例模式+策略模式+构建者模式+bugly

日志模块封封装:单例模式策略模式构建者模式bugly 一.单例模式策略模式构建者模式二.日志模块封装1.日志等级&#xff1a;LoggerLevel枚举类2.日志输出类型&#xff1a;LoggerType枚举类3.ILogger接口4.LogCatLogger/FileLogger/NetWorkLogger/EmailLogger5.使用构建者模式创建…

相同格式相同分辨率图片不同大小分析

1、问题 有三张图片&#xff0c;如下&#xff1a; 这三张图片均为jpg格式&#xff0c;分辨率均为1851*580&#xff0c;肉眼看不出区别。但是大小不同。 2号为217KB&#xff0c;4号为1.15MB&#xff0c;5号为1.06MB。 我们看下常规信息&#xff0c;先看2号&#xff1a; 可以…

初识Linux:第五篇

初识Linux&#xff1a;第五篇 初识Linux&#xff1a;第五篇1.Linux下的用户2.Linux权限管理2.1文件权限管理2.2文件权限的更改2.21改变文件访问权限属性2.22改变文件的身份 3.三个问题 总结 初识Linux&#xff1a;第五篇 &#x1f601;本篇主要介绍Linux权限的相关知识&#x1…

vue+elementui+nodejs高校校园在线打印预约系统

在线提交文档进行打印 首页简单介绍系统 语言 node.js 框架&#xff1a;Express 前端:Vue.js 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;VScode 顶部或主页按钮转到打印 用户可以登录 查看历史打印记录 用户分学生和非学生 学生可以享有优惠…

基于.NetCore开源的Windows的GIF录屏工具

推荐一个Github上Start超过20K的超火、好用的屏幕截图转换为 GIF 动图开源项目。 项目简介 这是基于.Net Core WPF 开发的、开源项目&#xff0c;可将屏幕截图转为 GIF 动画。它的核心功能是能够简单、快速地截取整个屏幕或者选定区域&#xff0c;并将其转为 GIF动画&#x…

编写 ROS 消息发布订阅器(五)

执行命令&#xff0c;指定目录添加cpp文件 cd ~/catkin_ws/src/beginner_tutorials如果没有src目录&#xff0c; 就自己创建一个目录叫src cd src/ vim talker.cpp 复制代码粘贴&#xff1a; #include "ros/ros.h" #include "std_msgs/String.h" int m…

C++/R 期末冲刺3h

C 1. 基础程序 #include "iostream" // C头文件 #include "stdio.h" // C 头文件 //using namespace std; // 命名空间// main() 是程序开始执行的地方int main() {std::cout << "Hello, World!" << "\n";return 0; …

【数据结构】线性结构 之 顺序表

&#x1f331;博客主页&#xff1a;大寄一场. &#x1f331;系列专栏&#xff1a;数据结构与算法 &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞⭐收藏➕关注 目录 前言 顺序表概念及结构 静态代码实现&#xff1a; 动态代码实现&#xff1a; SeqList.h文件 SeqLi…

使用VitePress和Github搭建个人博客网站,可以自动构建和发布

之前我们写过一篇关于如何自动构建和发布个人博客的文章&#xff0c;当时是使用VuePress和GitLab。GitLab持续集成部署CI/CD初探&#xff1a;如何自动构建和发布个人前端博客 现在换了Vue3和Vite&#xff0c;使用VitePress在Github上又搭建了一个博客。博客地址&#xff1a; …

博弈Ai官网ChatGPT能力真实测评

链接&#xff1a;https://chat.bo-e.com/&#xff08;基于ChatGPT4和3.5研发的智能聊天机器人国产镜像&#xff09; 一&#xff0c;博弈Ai的UI设计样式 1、博弈Ai&#xff08;ChatGPT&#xff09;白天模式 2、博弈Ai&#xff08;ChatGPT&#xff09;黑天模式 3、博弈Ai&#x…

五、c++学习(加餐1:汇编基础学习)

经过前面几节课的学习&#xff0c;我们在一些地方都会使用汇编来分析&#xff0c;我们学习汇编&#xff0c;只是学习一些基础&#xff0c;主要是在我们需要深入分析语法的时候&#xff0c;使用汇编分析&#xff0c;这样会让我们更熟悉c编译器和语法。 从这节课开始&#xff0c…

【003hive基础】hive的数据类型

文章目录 一.数据类型1. 基础数据类型2. 复杂数据类型 二. 显式转换与隐式转换三. hive的读时模式 一.数据类型 1. 基础数据类型 2. 复杂数据类型 array: 有序相同数据类型的集合。 arrays(1, 2)map : key必须是基本数据类型&#xff0c;value不限。 map(‘a’, 1, ‘b’, 2)s…

线性回归、正规方程和梯度下降法

一、线性回归简介 1.定义与公式 线性回归是利用回归方程(函数)对一个或多个自变量(特征值)和因变量(目标值)之间关系进行建模的一种分析方式。 特点&#xff1a;只有一个自变量的情况称为单变量回归&#xff0c;多余一个自变量情况的叫做多元回归 通用公式&#xff1a; y …

javascript基础三:谈谈 JavaScript 中的类型转换机制

一、概述 JS中有六种简单数据类型&#xff1a;undefined、null、boolean、string、number、symbol&#xff0c;以及引用类型&#xff1a;object 但是我们在声明的时候只有一种数据类型&#xff0c;只有到运行期间才会确定当前类型 let name y?allen:1上面代码中&#xff0c…

2023年NOC大赛创客智慧编程赛项Python 复赛模拟题(二)

题目来自:NOC 大赛创客智慧编程赛项Python 复赛模拟题(二) NOC大赛创客智慧编程赛项Python 复赛模拟题(二) 第一题: 编写一个成绩评价系统,当输入语文、数学和英语三门课程成绩时,输出三门课程总成绩及其等级。 (1)程序提示用户输入三个数字,数字分别表示语文、数学、…

ChatGPT-4 镜像网站推荐

文章目录 1. TomChat2. Ai Doge3. 二狗问答4. 小莓用AI5. Ora6. 未知名字7. VIVI-AI8. ATALK9. ChatGPT Web10 AIchatOS 什么是ChatGPT? ChatGPT&#xff0c;全称&#xff1a;聊天生成预训练转换器&#xff08;英语&#xff1a;Chat Generative Pre-trained Transformer&#…

抓取领域相关论文及摘要

抓取规划问题是指确定物体与手指间的一系列接触位置&#xff0c;使得手指能抵抗任意外力且灵活操作物体的能力。传统的基于分析的抓取规划需要根据已知的被抓物体模型根据力闭合的条件判断抓取的好&#xff0c;这种方法只适合对已知的物体进行抓取。 然而日常生活中有很多相似…

MyBatis 中的动态 SQL 是什么?它的作用是什么?

MyBatis 中的动态 SQL 是一种允许在 SQL 语句中根据不同的条件动态生成 SQL 语句的技术。它可以根据不同的条件生成不同的 SQL 语句&#xff0c;从而达到灵活构建 SQL 语句的目的。动态 SQL 可以减少代码的重复度&#xff0c;提高代码的可维护性和可读性。 动态 SQL 使用 OGNL…