一、概述

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

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

后端随机生成抠图和带有抠图阴影的背景图片,后台保存随机抠图位置坐标
前端实现滑动交互,将抠图拼在抠图阴影之上,获取到用户滑动距离值,比如上述示例
前端将用户滑动距离值传入后端,后端校验误差是否在容许范围内。
        这里单纯校验用户滑动距离是最基本的校验,出于更高的安全考虑,可能还会考虑用户滑动的整个轨迹,用户在当前页面的访问行为等。这些可以很复杂,甚至借助到用户行为数据分析模型,最终的目标都是增加非法的模拟和绕过的难度。这些有机会可以再归纳总结常用到的方法,本文重点集中在如何基于 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. 2019年1月计算机书籍JavaScript新书
  2. Mybatis简约执行过程
  3. php msf dev product,3 框架运行环境
  4. 关于类黄酮和类胡萝卜素
  5. 我是如何学习写一个操作系统(二):操作系统的启动之Bootloader
  6. double处理arithmeticexception为什么不报错_板式换热器为什么冷热不均匀?应怎样检查并简单处理?...
  7. 浅析STM32之usbh_def.H
  8. 梁鑫:重构 - 在美股行情系统的实践
  9. apache性能优化
  10. 介绍6款热门的SpringCloud微服务开源项目,总有适合你的!
  11. leetcode - 4. Median of Two Sorted Arrays
  12. 最新第一波:全国信息化工程师软考-系统集成项目管理工程师(高级案例高分论文)
  13. 明御安全网关(IPS)批量导入黑名单IP
  14. Mothur3进阶_Mothur扩增子基因序列处理_数据比对、聚类及其处理评估
  15. 计算机组成原理之概述篇(一)
  16. IDEA怎么查看现在的项目使用的JDK版本?
  17. QQ圈子,是在这里在一起,还是在那时在一起?
  18. 网吧服务器点歌系统,网吧点歌系统_网吧语音大师_蓝宝石语音_网吧点歌系统_蓝宝石呼叫网管_hylbs.com...
  19. 安卓手机可以运行linux程序吗,重磅!安卓系统也能运行PC程序了,实测效果令人惊在当场!...
  20. Prometheus:监控与告警:10:Node Exporter的Grafana模版

热门文章

  1. 风力发电机高散热滑环
  2. excel添加列下拉框票价_Excel多行多列数据源下拉菜单-excel设置下拉菜单
  3. Python字符串的表示方法
  4. 华为新系统鸿蒙寓意,华为鸿蒙新名字正式宣布?寓意深远让人眼前一亮,这一次真的稳了...
  5. iOS开发-文件管理(一)
  6. 「生涯规划」与「职业生涯规划」的差异
  7. MOS与IC的选型公司
  8. Word中批量设置参考文献为上标
  9. php批量添加图片,php excel给excel批量插入图片
  10. Vue基础知识总结(二):进阶篇