Canvas系列-滑动验证码

1,827 阅读4分钟

前言

 验证码是一种区分用户是计算机还是人的验证程序,区分用户是真人还是程序,防止程序频繁访问服务器占用过多的资源。一般防止恶意破解密码、刷票、论坛灌水等,有效防止对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试,防止恶意注册等。

 应用场景适用于登录、注册、活动、论坛、短信等高风险业务场景,例如有个短信接收验证码的功能,众所周知这个功能每条短信都是要收费的,你不做验证处理控制频率的话,如果被人恶意用脚本触发的话,一秒几万短信,分分钟让你濒临破产边缘,这谁扛得住啊。

 那么接下来就使用Canvas来模拟实现一个滑动验证功能,后面几篇文章都会谈论如何实现各种验验证码,随机字符串验证码、加减算术验证码、文字点选验证码等。

 之前有写过Canvas系列的一些实用小功能原理,有兴趣的可以瞧上一瞧~

实现滑动验证功能

 效果图如下,源码链接

canvas滑动验证演示.gif

基本思路

1、布局分为canvas图形和滑动上下两部分,上面为两个重合定位的canvas容器,下部分为可拖动的滑块

2、两个canvas设置同样大小,绘制同样的背景图,然后绘制同样的凹凸块,接下来把其中一个裁剪出来,重新设置凹凸块canvas的宽度

3、拖动下面滑动块,然后让裁剪的canvas滑动块同步滑动

4、判断凹凸块的定位,如果在阀值范围内则验证成功,改变相应状态,否则验证失败,然后重新刷新背景和凹凸块位置

下面就按照基本思路来一步步实现~

第一步:实现布局
<div class="verify-container" :style="{width: `${width}px`}">
  <!-- 刷新按钮 -->
  <div class="refresh" @click="reset">
    <svg t="1637315258145" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2420" width="20" height="20"><path d="M960 416V192l-73.056 73.056a447.712 447.712 0 0 0-373.6-201.088C265.92 63.968 65.312 264.544 65.312 512S265.92 960.032 513.344 960.032a448.064 448.064 0 0 0 415.232-279.488 38.368 38.368 0 1 0-71.136-28.896 371.36 371.36 0 0 1-344.096 231.584C308.32 883.232 142.112 717.024 142.112 512S308.32 140.768 513.344 140.768c132.448 0 251.936 70.08 318.016 179.84L736 416h224z" p-id="2421" fill="#8a8a8a"></path></svg>
  </div>
  <div class="pic">
    <canvas class="canvas_img" ref="canvas_img" :width="width" :height="height"></canvas>
    <canvas class="canvas_block" ref="canvas_block" :width="width" :height="height" :style="{left: blockLeft+'px'}"></canvas>
  </div>
  <!-- 滑动栏 -->
  <div class="slider" :style="{height: blockW+'px'}">
    <div class="tip" v-if="showText">向右滑动完成验证</div>
    <div :class="['bar', slideState]" :style="{width: sliderLeft + 'px'}"></div>
    <div :class="['slider-icon', slideState]"
      :style="{left: sliderLeft + 'px'}" 
      @touchstart="touchStart"
      @touchmove="touchMove"
      @touchEnd="touchEnd">
      {{ { active: '>', fail: 'x', success: '√' }[slideState] || '>' }}
    </div>
    <!-- <div ref="slider-icon"
      :class="['slider-icon', slideState]"
      :style="{left: sliderLeft + 'px'}">
      >
    </div> -->
  </div>
</div>

上面为滑动验证的HTML代码,样式就不展示了,可以查看源码

第二步:绘制Canvas背景和凹凸块
<script>
  const App = {
    props: {
      width: {
        type: Number,
        default: 320
      },
      height: {
        type: Number,
        default: 160
      },
      blockW: { // 裁剪canvas宽高
        type: Number,
        default: 40
      },
      accuracy: {  // 精度
        type: Number,
        default: 1
      },
      images: {  // 背景图
        type: Array,
        default: [
          'https://img2.baidu.com/it/u=172118635,3843440198&fm=26&fmt=auto',
          'https://img2.baidu.com/it/u=2726247805,538885610&fm=26&fmt=auto',
          'https://img1.baidu.com/it/u=1078976348,1462740125&fm=26&fmt=auto'
        ]
      }
    },
    data() {
      return {
        bgImg: null,  // 背景图
        ctxImg: null, // 背景画笔
        ctxBlock: null, // 滑块画笔

        blockRect: {  // 滑块宽、圆半径、坐标
          w: this.blockW + 2 * this.blockW / 4,
          r: this.blockW / 4,
          x: 0,
          y: 0
        },
        blockLeft: 0, // 裁剪后left属性
        startX: 0, // 滑动起点
        EndX: 0, // 结束位置
        sliderLeft: 0,  // 拖动滑块的滑动距离
        slideState: '' , // success fail active
        timeIns: null,
        showText: true, // 是否显示滑动提示
        isMouseDown: false,
      }
    },
    mounted () {
      this.init()

      // 如果是pc端则用mouse事件
      // this.mouseEvent()
    },
    beforeDestroy () {
      clearTimeout(this.timeIns)
    },
    methods: {
      init () {
        this.ctxImg = this.$refs['canvas_img'].getContext('2d');
        this.ctxBlock = this.$refs['canvas_block'].getContext('2d');

        this.getImg()
      },
      // 获取背景图
      getImg () {
        const img = document.createElement('img');
        const imagesLen = this.images.length;
        const randomIndex = Math.floor(Math.random() * imagesLen);
        img.crossOrigin = "Anonymous"; 
        img.src = this.images[randomIndex];
        this.bgImg = img;

        img.onload = () => {
          console.log('图片加载完成')
          this.ctxImg.drawImage(this.bgImg, 0, 0, this.width, this.height); // 先绘制背景再绘制凹凸块
          this.getBlockPostion()
          this.ctxBlock.drawImage(this.bgImg, 0, 0, this.width, this.height);

          // console.log(this.blockRect.x, this.blockRect.y, this.blockRect.w)
          const _yPos = this.blockRect.y - 2 * this.blockRect.r;
          const imageData = this.ctxBlock.getImageData(this.blockRect.x, _yPos, this.blockRect.w, this.blockRect.w + 1);
          this.$refs['canvas_block'].width = this.blockRect.w;
          this.ctxBlock.putImageData(imageData, 0, _yPos);
        }
        console.log(this.bgImg)
      },
      // 获取凹凸块的位置
      getBlockPostion () {
        const xPos = Math.floor(this.width / 2 +  Math.random() * (this.width / 2 - this.blockRect.w));
        const yPos = Math.floor(30 + Math.random() * (this.height - this.blockRect.w -30));
        // console.log(xPos, yPos)
        this.blockRect.x = xPos;
        this.blockRect.y = yPos;

        this.draw(this.ctxImg, 'fill');
        this.draw(this.ctxBlock, 'clip');
      },
      draw (ctx, operation) {
        const { r, x, y } = this.blockRect;
        const _w = this.blockW;
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.arc(x + _w / 2, y - r + 2, r, 0.72 * Math.PI, 2.26 * Math.PI);
        ctx.lineTo(x + _w, y);
        ctx.arc(x + _w + r - 2, y + _w / 2, r, 1.21 * Math.PI, 2.78 * Math.PI);
        ctx.lineTo(x + _w, y + _w);
        ctx.lineTo(x, y + _w);
        ctx.arc(x + r - 2, y + _w / 2, r + 0.4, 2.76 * Math.PI, 1.24 * Math.PI, true);
        ctx.lineTo(x, y);
        ctx.closePath();
        ctx.fillStyle = 'rgba(225, 225, 225, 0.8)';
        ctx.strokeStyle = 'rgba(225, 225, 225, 0.8)';
        ctx.lineWidth = 2;
        ctx.stroke();
        ctx[operation]();
      }
    }
  }
  Vue.createApp(App).mount('#app');
</script>

解析:

  • 先随机获取背景图,等图片加载完成后使用drawImage()方法绘制背景图片,注意底部的canvas背景要先绘制图片,再绘制凹凸块;
  • 绘制完背景图片后再随便获取绘制凹凸块的坐标;
  • 获取完坐标后调用draw函数绘制凹凸块,底部的canvas背景图使用fill()方法绘制,需要滑动的canvas使用clip()方法裁剪出凹凸快形状;
  • 再通过getImageData()方法和putImageData()方法把图片信息绘制到需要滑动的凹凸块上,并设置canvas宽度为滑块宽,并且定位到左侧就可以了。
第三步:拖动滑块

移动端我们使用到 touchstarttouchmovetouchend 事件,如果在pc端就要用到mousedownmousemovemouseup事件。这里我们只说移动端的情况,pc端请查看源码

// mobile
touchStart (e) {
  // console.log(e)
  this.startX = e.changedTouches[0].pageX;
  this.showText = false;
},
touchMove (e) {
  this.endX = e.changedTouches[0].pageX - this.startX;
  // 禁止超出边界
  if (this.endX < 0 || this.endX > this.width - this.blockW) {
    return
  }
  // 拖动的距离
  this.sliderLeft = this.endX;
  this.blockLeft = this.sliderLeft / (this.width - this.blockW) * (this.width - this.blockRect.w);
  this.slideState = 'active';
},
touchEnd (e) {
  const isPass = this.verify()
  console.log(isPass)
  if (isPass) {
    this.slideState = 'success';
  } else {
    this.slideState = 'fail';
    // 如果失败则1000毫秒后重置
    this.timeIns = setTimeout(() => {
        this.reset()
    }, 1000)
  }
}
第四步:验证

第三步我们拖动滑块,当释放滑块时(touched事件)我们就开始根据传入的精度阀值验证,如果在阀值内则表明成功,否则失败。如果验证失败我们则重新刷新。

// 判断精度
verify () {
  // console.log(Math.abs(this.blockLeft - this.blockRect.x))
  return Math.abs(this.blockLeft - this.blockRect.x) <= this.accuracy
}
第五步:刷新重置

重置即调用clearRect()方法清空canvas画布,然后初始化所有变量数据。

// 重置
reset () {
  this.showText = true;
  this.slideState = '';
  this.sliderLeft = 0;
  this.blockLeft = 0;
  this.$refs['canvas_block'].width = this.width;
  this.ctxImg.clearRect(0, 0, this.width, this.height);
  this.ctxBlock.clearRect(0, 0, this.width, this.height);
  this.getImg();
}

结尾

上面就是【滑动验证】的实现原理,代码是使用vue编写的小demo,可能存在一些兼容性问题,也没有封装成组件,但是具有一些参考意义,用于生产可以自己去封装成组件使用,完整的代码在我的GitHub仓库


本文是笔者总结编撰,如有偏颇,欢迎留言指正,若您觉得本文对你有用,不妨点个赞~

关于作者: GitHub 简书 掘金