[Vue3] Swipe verification component | Swipe verification

foreword

  • Slider verification is not only to judge whether to slide to the end, but the real purpose is to detect user behavior, whether the detection behavior is man-made, scripted, or other.

  • Prevent mass registrations, requests, etc. using scripts. For example, when sending a request, determine how long the user stays on a certain page. Whether the login and registration buttons are clicked when logging in and registering. If the login and registration requests are sent directly without clicking, then this behavior is probably script or machine behavior.

  • Slider validation has several important data

    1. start of slider
    2. end point of the slider
    3. The time it takes for the slider to slide from the starting point to the end point. For example, a slider with an artificial sliding length of 240px needs at least 50 milliseconds to slide from the starting point to the end point. If it takes less than 50 milliseconds from the start point to the end point, then 99% of this behavior is machine and script behavior. Artificially slipping is not so fast.
    4. The length of the slider, the longer the length of the slider, the better, because the longer the length, the more data we collect, and the more data, the easier it is to judge whether it is human-made or machine or script behavior. The slider should preferably be above 200px+.
    5. The track of sliding the slider, such as sliding a slider with a length of 300px, can collect at least 20 points, and each point has the x and y axis position of the user's sliding, sliding time, etc.

Ways of identifying

Human sliding trajectory reference

When we slide the verification slider by hand, the sliding track is a variable speed curve movement , with uneven distribution of ups and downs, and variable speed movement. As shown in Figure 3

figure 1
insert image description here

figure 2
insert image description here

image 3
insert image description here

Man-made/man-machine/script sliding judgment

  1. If the time taken to slide the slider is less than 50 milliseconds, it is judged as man-machine/script. For a 200px slider, the artificial sliding is not so fast.
  2. Judging the ups and downs of the sliding line, if it is a pure straight line, it is human-machine, because the artificial sliding is not so straight.
  3. Calculating the sliding speed, artificial sliding is definitely a variable speed sliding , and if it is a uniform sliding, it is man-machine.

If it is not artificially sliding, most of them slide in a straight line at a constant speed without ups and downs. For example script swipe.
However, some machines/scripts can simulate human sliding, so it is easy to crack the slider verification.

  • The following is a series of artificial sliding data, rendered with canvs,
    • x - x-axis position,
    • y - the y-axis position,
    • moveTime - the time to scroll to this point
 <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>

rendering
insert image description here

Swipe verification (vue3)

Props

props type Defaults effect
width Number 300 slider width
height Number 45 slider height
servertest Blooear false Whether to enable backend verification - click me to jump to details
drop-color String #fff slider color
tip-none-color String #000 Text color to be verified
tip-suc-color String #fff Authentication success text color
tip-test-ing-color String #fff text color in validation
tip-tail-color String #ee0a24 Validation failed text color
slide-color String #ee0a24 slider background color
success-bg-color String #07c160 Validation passed background color
tail-bg-color String #ee0a24 Validation failed background color
active-bg-color String #1989fa Activated background color
test-ing-bg-color String #ff976a Active background color in validation
font-size Number 16 text size
test-tip String Verifying... Validation Prompt Text
tip-txt String Swipe right to verify Prompt text to be verified
success-tip String Great, congratulations on passing the verification! Verification successful text prompt
fail-tip String Verification failed, please try again Verification failed text prompt

event

event effect parameter Remark
state verification status (vfcStatu, slideInfo )vfcStatu.statu → Verification status. slideInfo → slide track information vfcStatu.statu has 4 states. tail- Verification failed ② success- Verification succeeded ③ testing- Front-end verification is in progress ④ servertest- In back-end verification, the back-end verification will only be triggered if the servertest setting of props is not true ( if servertest is true, if the vfcStatu.statu status is not set, the animation will always be in verification )

vue validation component

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>

Use swipe to verify

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>

Success renderings

insert image description here

Failure rendering

insert image description here

Effect picture in verification

Validation will have a transparency effect
insert image description here

With backend verification (axios | express

  • With back-end verification, axios and express are mainly used. The principle of back-end verification is the same as that of front-end verification, but the verification is done in a different place.
  • But it is still not safe, because the front end can forge data, and then send a request to the back end, and the back end returns data.
  • Pure sliders still need to be used with detection behaviors, because sliders are only one of the multi-ring verifications when accessing a certain page or requesting a certain data, such as how long the user stays on a certain page, various factors come Determine whether the current behavior is human, or script\machine behavior, etc.

backend code

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

Example: front-end login code

need:

  1. The user name and password entered by the user to log in are legal
  2. When the user clicks the login button, instead of directly initiating a login request to the backend, a verification slider pops up
  3. Successful verification → initiate a login request
  4. Verification successful → Prompt that verification failed, no login request is initiated

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> 

before input
insert image description here

The information entered is legal + after clicking the login button
insert image description here

Verify successful login request
insert image description here

verification failed
insert image description here

Finished product display

before login

insert image description here

When logging in without entering login information

insert image description here

When the entered information is invalid

  • The input information is invalid and the slider verification should not pop up
    insert image description here

When the input information is legal

insert image description here

when verification is successful

If the verification is successful, it will automatically log in. On the contrary, if the verification is unsuccessful, no login request will be initiated and a re-verification will be prompted
insert image description here

Guess you like

Origin blog.csdn.net/qq_43614372/article/details/130763088