Canvas系列-滑动验证码

前言

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

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

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

 之前有写过Canvas系列的一些实用小功能原理,有兴趣的可以瞧上一瞧~
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
简书
掘金

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容