一、概述

滑动验证码在很多网站流行,一方面对用户体验来说,比较新颖,操作简单,另一方面相对图形验证码来说,安全性并没有很大的降低。当然到目前为止,没有绝对的安全验证,只是不断增加攻击者的绕过成本。

二、原理分析    
接下来分析下滑动验证码的核心流程:

后端随机生成抠图和带有抠图阴影的背景图片,后台保存随机抠图位置坐标
前端实现滑动交互,将抠图拼在抠图阴影之上,获取到用户滑动距离值,比如上述示例
前端将用户滑动距离值传入后端,后端校验误差是否在容许范围内。
        这里单纯校验用户滑动距离是最基本的校验,出于更高的安全考虑,可能还会考虑用户滑动的整个轨迹,用户在当前页面的访问行为等。这些可以很复杂,甚至借助到用户行为数据分析模型,最终的目标都是增加非法的模拟和绕过的难度。这些有机会可以再归纳总结常用到的方法,本文重点集中在如何基于 Java 来一步步实现滑动验证码的生成。

可以看到,滑动图形验证码,重要有两个图片组成,抠块和带有抠块阴影的原图,这里面有两个重要特性保证被暴力和 (破) 谐 (解) 的难度:抠块的形状随机和抠块所在原图的位置随机。这样就可以在有限的图集中制造出随机的、无规律可寻的抠图和原图的配对。

三、代码实现
    用代码如何从一张大图中抠出一个有特定随机形状的小图呢?

第一步,先确定一个抠出图的轮廓,方便后续真正开始执行图片处理操作

图片是有像素组成,每个像素点对应一种颜色,颜色可以用 RGB 形式表示,外加一个透明度,把一张图理解成一个平面图形,左上角为原点,向右 x 轴,向下 y 轴,一个坐标值对应该位置像素点的颜色,这样就可以把一张图转换成一个二维数组。基于这个考虑,轮廓也用二维数组来表示,轮廓内元素值为 1,轮廓外元素值对应 0。

这时候就要想这个轮廓形状怎么生成了。有坐标系、有矩形、有圆形,没错,用到数学的图形函数。典型用到一个圆的函数方程和矩形的边线的函数,类似:

(x-a)²+(y-b)²=r² 中,有三个参数 a、b、r,即圆心坐标为 (a,b),半径 r。这些将抠图放在上文描述的坐标系上很容易就图算出来具体的值。

示例代码如下:
css 样式:

  .block {position: absolute;left: 0;top: 0;cursor: pointer;cursor: grab;}.block:active {cursor: grabbing;}.sliderContainer {position: relative;text-align: center;width: 310px;height: 40px;line-height: 40px;margin-top: 15px;background: #f7f9fa;color: #45494c;border: 1px solid #e4e7eb;margin-top: 180px;}.sliderContainer_active .slider {height: 38px;top: -1px;border: 1px solid #1991FA;}.sliderContainer_active .sliderMask {height: 38px;border-width: 1px;}.sliderContainer_success .slider {height: 38px;top: -1px;border: 1px solid #52CCBA;background-color: #52CCBA !important;color: #fff;}.sliderContainer_success .sliderMask {height: 38px;border: 1px solid #52CCBA;background-color: #D2F4EF;}.sliderContainer_success .sliderIcon {display: none;}.sliderContainer_fail .slider {height: 38px;top: -1px;border: 1px solid #f57a7a;background-color: #f57a7a !important;}.sliderContainer_fail .sliderMask {height: 38px;border: 1px solid #f57a7a;background-color: #fce1e1;}.sliderContainer_fail .sliderIcon {top: 14px;background-position: 0 -82px !important;}.sliderContainer_active .sliderText, .sliderContainer_success .sliderText, .sliderContainer_fail .sliderText {display: none;}.sliderMask {position: absolute;left: 0;top: 0;height: 40px;border: 0 solid #1991FA;background: #D1E9FE;}.slider {position: absolute;top: 0;left: 0;width: 40px;height: 40px;background: #fff;box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);transition: background .2s linear;cursor: pointer;cursor: grab;}.slider:active {cursor: grabbing;}.slider:hover {background: #1991FA;}.slider:hover .sliderIcon {background-position: 0 -13px;}.sliderIcon {position: absolute;top: 15px;left: 13px;width: 14px;height: 12px;background: url("//static1.bitautoimg.com/yc-common/web-component/components-silderVerify-pm/img/icon_light.f13cff3.png") 0 -26px;background-size: 34px 471px;}.refreshIcon {position: absolute;right: 0;top: 0;width: 34px;height: 34px;cursor: pointer;background: url("//static1.bitautoimg.com/yc-common/web-component/components-silderVerify-pm/img/icon_light.f13cff3.png") 0 -437px;background-size: 34px 471px;z-index: 1000;}.captcha { position: fixed; width: 310px; height: 155px; top: 50%; left: 50%; margin-left: -155px; margin-top: -200px; z-index: 80; }.captcha::before{content: '';position: absolute;width: 44px;height: 44px;background-image: url("//static1.bitautoimg.com/yc-common/web-component/components-silderVerify-pm/img/close.png");background-repeat: no-repeat;background-size: 44px auto;bottom: -140px;left: 50%;margin-left: -22px;pointer-events: none;}.canvasCtx { position: absolute; width:310px; top: 0; left: 0;}.blockCtx { position: absolute; width:310px; top: 0; left: 0;}.silder-mark {position: fixed;top: 0;left: 0;width: 100%;height: 100%;z-index: 79;background: rgba(0, 0, 0, 0.6);}

commons.js 公共类

/*** 加载图片* @param url {string} 地址  * @param fn {function} 回调方法  */
function imageLoad(url,fn){let img = new Image()img.onload = function () { fn(img)}img.onerror = () => {img.setSrc(url)}img.setSrc = function (src) {if(yicheUtils.isIE()){  // IE浏览器无法通过img.crossOrigin跨域,使用ajax获取图片blob然后转为dataURL显示let xhr = new XMLHttpRequest()xhr.onloadend = function (e) {let file = new FileReader() // FileReader仅支持IE10+file.readAsDataURL(e.target.response)file.onloadend = function (e) {img.src = e.target.result}}xhr.open('GET', src)xhr.responseType = 'blob'xhr.send()} else img.src = src}img.setSrc(url)
}/*** 随机生成图片地址* return {string}*/
function getRandomImgSrc(){return `http://image.bitautoimg.com/weixin/dynamic/${createRandom(1, 20)}-310x155.jpg`
}/*** @description: 创建随机数* @param min {int} 最小* @param max {int} 最大* return {int} 随机数*/
function createRandom(min, max){return Math.round(Math.random() * (max - min) + min)
}
/*** 求和* @param {*} x * @param {*} y */
function sum(x, y) { return x + y
}/*** 乘* @param {*} x */
function square(x){return x * x
}module.exports =  {//加载图片imageLoad,//随机生成图片地址getRandomImgSrc,//创建随机数createRandom,//求和sum,//乘square
}

client.js 客户度类

/*** @description: 识别客户端版本类* @param agents {array} 包含的APP名称集合* return {boolen}*/
function isAppBase(agents){var ua = navigator.userAgent;if (!(agents && agents.length > 0 && ua)) return falsevar result = agents.some(item => ua.toLowerCase().indexOf(item.toLowerCase()) >= 0)return result
}/*** @description: 是否是安卓系统* return {boolen}*/
function isAndroid(){return isAppBase(['Android'])
}/*** @description: 是否是IOS* return {boolen}*/
function isIos(){return isAppBase(['iPhone', 'iPad', 'iPod'])
}/*** @description: 是否是APP* return {boolen}*/
function isMobile(){return isAppBase(['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod'])
}/*** @description: 是否是微信* return {boolen}*/
function isWeChat(){return isAppBase(['MicroMessenger'])
}/*** @description: 是否是IE* return {boolen}*/
function isIE(){return isAppBase(['Trident'])
}/*** 获取浏览器类型* return {string} 浏览器类型*/
function getBrowserType(){var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串var isOpera = userAgent.indexOf("Opera") > -1; //判断是否Opera浏览器var isIE = userAgent.indexOf("compatible") > -1&& userAgent.indexOf("MSIE") > -1 && !isOpera; //判断是否IE浏览器var isEdge = userAgent.indexOf("Edge") > -1; //判断是否IE的Edge浏览器var isFF = userAgent.indexOf("Firefox") > -1; //判断是否Firefox浏览器var isSafari = userAgent.indexOf("Safari") > -1&& userAgent.indexOf("Chrome") == -1; //判断是否Safari浏览器var isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1; //判断Chrome浏览器if (isIE) {var reIE = new RegExp("MSIE (\\d+\\.\\d+);");reIE.test(userAgent);var fIEVersion = parseFloat(RegExp["$1"]);if (fIEVersion == 7) {return "IE7";} else if (fIEVersion == 8) {return "IE8";} else if (fIEVersion == 9) {return "IE9";} else if (fIEVersion == 10) {return "IE10";} else if (fIEVersion == 11) {return "IE11";} else {return "0";}//IE版本过低return "IE";}if (isOpera) {return "Opera";}if (isEdge) {return "Edge";}if (isFF) {return "FF";}if (isSafari) {return "Safari";}if (isChrome) {return "Chrome";}
}module.exports =  {//是否是IEisIE
}

template.js 模版类

function silderTemplate(){return `<!---黑色蔗渣层 start---><div class="silder-mark"></div><!---黑色蔗渣层 end---><!---滑动验证码 start---><div class="captcha"><div class="refreshIcon"></div><canvas class="canvasCtx"></canvas><canvas class="blockCtx"></canvas><div class="sliderContainer"><div class="sliderMask"><div class="slider"><span class="sliderIcon"></span></div></div><span class="sliderText">向右滑动填充拼图</span></div></div><!---滑动验证码 end --->`
}module.exports =  {//滑动容器模版silderTemplate
}

index.js 主体代码

//模版类
const { silderTemplate } = require("./template")
//公共方法
const { imageLoad, getRandomImgSrc,createRandom, sum, square } = require("./commons")
/*** 滑动验证码组件* return this*/
function SilderVerify(options){var that = thisvar setting = {//容器名称containerName: 'dynamicVerify_view',//父级属性root: document.body,//自定义绑定事件eventName: '[action=silderVerify]',//canvas宽度w: 310,//canvas高度h: 155,// 滑块边长l: 42,//滑块半径r: 9,//X轴x: 0,//Y轴y: 0,//圆周率PI: Math.PI,//成功回调onSuccess: function(){console.log("成功")},//失败回调onFail:function(){console.log('失败')},//刷新回调onRefresh:function(){console.log('刷新')},//关闭回调onClose:function(){console.log("关闭")}}that.options = yicheUtils.extend(options,setting)//初始化控件that.init()
}/*** 初始化控件*/
SilderVerify.prototype.init = function(){var that = this//初始化属性that.initProp()//初始化事件that.initEvent()
}/*** 初始化事件*/
SilderVerify.prototype.initEvent = function(){var that = this/*************注册事件 start*********** */zQuery.off(that.options.root,'click', that.options.eventName)zQuery.on(that.options.root,'click', that.options.eventName,function(ev){let _this = this//显示分享层that.showModel();})/*************注册事件 end*********** */}/*** 打开验证层*/
SilderVerify.prototype.showModel = function(){var that = this//开始时间that.startDate = new Date()//渲染模版that.readerView()
}/*** 关闭验证层*/
SilderVerify.prototype.closeModel = function(){var that = thiszQuery.hide(that.container)//关闭回调事件that.options.onClose && that.options.onClose()
}/*** 渲染弹层模版*/
SilderVerify.prototype.readerView = function(){var that = thisthat.container = zQuery.find(that.options.root,`.${that.options.containerName}`)[0]if(!that.container){that.container = document.createElement('div')zQuery.addClass(that.container,that.options.containerName) that.options.root.appendChild(that.container)}that.container.innerHTML = yicheUtils.miniTpl(silderTemplate())//初始化canvasthat.initDOM()//初始化图片that.initImg()//初始化事件that.bindEvents()//显示层zQuery.show(that.container)
}/*** 初始化canvas*/
SilderVerify.prototype.initDOM = function(){let that = thislet w = that.wlet h = that.hconst captcha = zQuery.find(that.container, '.captcha')[0]const canvas = zQuery.find(that.container,'.canvasCtx')[0] // 画布if (!canvas) { console.log("没找到canvasCtx"); return false } canvas.style.width = w + 'px'canvas.style.height = h + 'px'const block = zQuery.find(that.container,'.blockCtx')[0] // 滑块if (!block) { console.log("没找到blockCtx"); return false } let sliderContainer = zQuery.find(that.container,'.sliderContainer')[0]if (!sliderContainer) { console.log("没找到sliderContainer"); return false } sliderContainer.style.width = w + 'px'let refreshIcon = zQuery.find(that.container,'.refreshIcon')[0]let slider = zQuery.find(that.container,'.slider')[0]let sliderMask = zQuery.find(that.container,'.sliderMask')[0]let sliderIcon = zQuery.find(that.container,'.sliderIcon')[0]let mark = zQuery.find(that.container,'.mark')[0]Object.assign(that, { canvas, block, sliderContainer, refreshIcon,slider, sliderMask, sliderIcon,captcha, mark, canvasCtx: canvas.getContext('2d'),blockCtx: block.getContext('2d')})return true
}/*** 初始化图片*/
SilderVerify.prototype.initImg = function(){let that = thislet w = that.wlet h = that.hlet x = that.ximageLoad(getRandomImgSrc(), function(img){that.draw()that.canvasCtx.drawImage(img, 0, 0, w, h)//精准切块that.blockCtx.drawImage(img, x, 0, w, h, 0, 0, w, h)})
}
/*** 初始化属性*/
SilderVerify.prototype.initProp = function(){var that = thislet { w, h, r ,l, x, y } = that.options//滑块实际边长that.L = l + r * 2 + 3that.w = wthat.h = hthat.r = rthat.l = lthat.x = xthat.y = y
}/*** 画图像*/
SilderVerify.prototype.draw = function(){let that = this// 随机创建滑块的位置that.x = createRandom(that.L + 10, that.w - (that.L + 10))that.y = createRandom(10 + that.r * 2, that.h - (that.L + 10))that.drawCtx(that.canvasCtx, that.x, that.y, 'fill')that.drawCtx(that.blockCtx, 0, that.y, 'clip')
}SilderVerify.prototype.drawCtx = function(ctx, x, y, operation){let that = thislet r = that.rlet PI = that.PIlet l = that.lctx.beginPath()ctx.moveTo(x, y)ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI)ctx.lineTo(x + l, y)ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI)ctx.lineTo(x + l, y + l)ctx.lineTo(x, y + l)ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true)ctx.lineTo(x, y)ctx.lineWidth = 2ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'ctx.stroke()ctx[operation]()ctx.globalCompositeOperation = 'destination-over'
}SilderVerify.prototype.clean = function(){let that = thislet w = that.wlet h = that.hthat.canvasCtx.clearRect(0, 0, w, h)that.blockCtx.clearRect(0, 0, w, h)that.block.width = w
}SilderVerify.prototype.reset = function(){let that = this//开始时间that.startDate = new Date()that.sliderContainer.className = 'sliderContainer'that.slider.style.left = 0that.block.style.left = 0that.sliderMask.style.width = 0that.clean()that.initImg()
}SilderVerify.prototype.verify = function(){let that = thisconst arr = that.trail // 拖动时y轴的移动距离const average = arr.reduce(sum) / arr.lengthconst deviations = arr.map(x => x - average)const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length)const left = parseInt(that.block.style.left)return {timeout: new Date().getTime() - that.startDate.getTime(),spliced: Math.abs(left - that.x) < 10,verified: stddev !== 0, // 简单验证拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作}
}/*** 初始化事件*/
SilderVerify.prototype.bindEvents = function(){let that = this//注册关闭层事件zQuery.on(that.container,'click','.silder-mark',function(ev){ev.preventDefault();that.closeModel()})that.captcha.onselectstart = () => false//注册刷新验证码zQuery.on(that.container,'click','.refreshIcon',function(ev){that.reset()that.options.onRefresh && that.options.onRefresh.call(that)})let originX, originY, trail = [], isMouseDown = falseconst handleDragStart = function (e) {e.preventDefault();originX = e.clientX || e.touches[0].clientXoriginY = e.clientY || e.touches[0].clientYisMouseDown = true}const handleDragMove = (e) => {let that = thise.preventDefault();let w = that.wif (!isMouseDown) return falseconst eventX = e.clientX || e.touches[0].clientXconst eventY = e.clientY || e.touches[0].clientYconst moveX = eventX - originXconst moveY = eventY - originYif (moveX < 0 || moveX + 38 >= w) return falsethat.slider.style.left = moveX + 'px'that.block.style.left = moveX + 'px'zQuery.addClass(that.sliderContainer, 'sliderContainer_active')that.sliderMask.style.width = moveX + 'px'trail.push(moveY)}const handleDragEnd = (e) => {if (!isMouseDown) return falseisMouseDown = falseconst eventX = e.clientX || e.changedTouches[0].clientXif (eventX === originX) return falsezQuery.removeClass(that.sliderContainer, 'sliderContainer_active')that.trail = trailconst { spliced, verified, timeout } = that.verify()if (spliced) {if (verified) {if(timeout / 1000 < 10){zQuery.addClass(that.sliderContainer, 'sliderContainer_success')zQuery.html(that.slider,parseInt(timeout / 1000) + "s") typeof that.options.onSuccess === 'function' && that.options.onSuccess()setTimeout(() => {that.closeModel()}, 1000)}else{zQuery.addClass(that.sliderContainer, 'sliderContainer_fail')typeof that.options.onFail === 'function' && that.options.onFail()setTimeout(() => {that.reset()}, 1000)}} else {zQuery.addClass(that.sliderContainer, 'sliderContainer_fail')that.reset()}} else {zQuery.addClass(that.sliderContainer, 'sliderContainer_fail')typeof that.options.onFail === 'function' && that.options.onFail()setTimeout(() => {that.reset()}, 1000)}}that.slider.addEventListener('mousedown', handleDragStart)that.slider.addEventListener('touchstart', handleDragStart,{ passive: false })that.block.addEventListener('mousedown', handleDragStart)that.block.addEventListener('touchstart', handleDragStart, { passive: false })document.addEventListener('mousemove', handleDragMove,{ passive: false })document.addEventListener('touchmove', handleDragMove,{ passive: false })document.addEventListener('mouseup', handleDragEnd)document.addEventListener('touchend', handleDragEnd)
}
window ? (window.SilderVerify = SilderVerify) :'';
module.exports = SilderVerify

手写滑动验证码,完整代码相关推荐

  1. 四、用简单神经网络识别手写数字(内含代码详解及订正)

    本博客主要内容为图书<神经网络与深度学习>和National Taiwan University (NTU)林轩田老师的<Machine Learning>的学习笔记,因此在全 ...

  2. 手写AspNetCore 认证授权代码

    在普通的MVC项目中 我们普遍的使用Cookie来作为认证授权方式,使用简单.登录成功后将用户信息写入Cookie:但当我们做WebApi的时候显然Cookie这种方式就有点不适用了. 在dotnet ...

  3. 手写数字识别项目代码——卷积神经网络LeNet-5模型

    ''' #2018-06-25 272015 June Monday the 26 week, the 176 day SZ 手写字体识别程序文件1: 这个程序使用了卷积神经网络LeNet - 5模型 ...

  4. 手写数字识别全部代码--全连接神经网络方法

    ''' #2018-06-25 272015 June Monday the 26 week, the 176 day SZ 手写字体识别程序文件1: 这个程序使用了全连接神经网络也就是DNN. 定义 ...

  5. 艺赛旗(RPA)RPA8.0 解决滑动验证码完整流程

    艺赛旗 RPA8.0全新首发免费下载 点击下载 http://www.i-search.com.cn/index.html?from=line1 前置(一个注意点) Note: 有一种情况,若 win ...

  6. 应用训练MNIST的CNN模型识别手写数字图片完整实例(图片来自网上)

    1 思考训练模型如何进行应用 通过CNN训练的MNIST模型如何应用来识别手写数字图片(图片来自网上)? 这个问题困扰了我2天,网上找的很多代码都是训练模型和调用模型包含在一个.py文件中,这样子每一 ...

  7. 基于CNN的MINIST手写数字识别项目代码以及原理详解

    文章目录 项目简介 项目下载地址 项目开发软件环境 项目开发硬件环境 前言 一.数据加载的作用 二.Pytorch进行数据加载所需工具 2.1 Dataset 2.2 Dataloader 2.3 T ...

  8. 【JS 纯手写轮播图代码】

    轮播图实现 首先需要在同级目录下创建img文件夹,用以储存你需要轮播的图片,注意设置好图片的宽度,以免出现空白区域.然后就可以愉快地实现轮播功能啦~ // An highlighted block & ...

  9. 一致性哈希算法 mysql_一致性哈希算法,在分布式开发中你必须会写,来看完整代码...

    今天我想先给大家科普下一致性哈希算法这块,因为我下一篇文章关于缓存的高可用需要用到这个,但是又不能直接在里面写太多的代码以及关于一致性hash原理的解读,这样会失去对于缓存高可用的理解而且会造成文章很 ...

最新文章

  1. Python可视化(matplotlib)图像自定义图例(Legend)
  2. Seconds_Behind_Master
  3. SparkProgrammingRDDs
  4. 对mysql的总结与反思_深入了解MySQL,一篇简短的总结
  5. pat 乙级 1021 个位数统计(C++)
  6. MongoDB学习2——Windows 使用mongo连接数据库
  7. python中进程创建—fork()
  8. 一文聊“图”,从图数据库到知识图谱
  9. 程序员笑话集锦之丈夫与妻子篇
  10. 春晚亲民,快手上行:探秘春晚红包的另一种打开方式
  11. UIDynamic(物理仿真)
  12. python实现一个土豆聊天 potato chat 机器人
  13. Windows网络编程之(二)Socket通信非阻塞模式Select(TCP和UDP)
  14. 计算机单机考试,信息技术考试系统(单机版)
  15. 当前网络上迅雷各版本实际效果研究报告
  16. 2的6次方怎么用计算机,2的6次方是多少(进制转换计算器)
  17. 2019年末,10 位院士对 AI 的深度把脉(下)
  18. “某某某”was not declared in this scope?报错原因。
  19. Python自动化修改word实例
  20. 物联网毕设(基于STM32的蓝牙检测心率+步数+手机APP)

热门文章

  1. matlab 保存数据路径,matlab中如何将uitable中的数据保存在指定路径下
  2. 人形时钟_Mecanim人形生物
  3. OpenCV实践:获取填空题的下划线
  4. 技能高考c语言程序填空题,技能练习题
  5. 自设标题栏随着布局向上滚动实现透明渐变
  6. 弘辽科技:淘宝开店可以改店名吗?怎么操作?
  7. 同账号不同服务器幻化T2,魔兽怀旧服TBC前瞻指南:520前夕开放太重要,千万别出门约会...
  8. 2008最新的浏览器市场份额统计
  9. java清除referer_Java实现跳过网站Referer校验
  10. FFmpeg多媒体格式分类详解