电脑和手机移动端都适用的jQuery拖动滑块图片拼图验证码插件,通过鼠标拖动或触屏滑动填充拼图来进行安全验证,点击刷新可以更换当前待验证的图片。
HTML & css:
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta content="width=device-width, initial-scale=1.0"> <title>拖动滑块图片拼图验证码插件</title> <!--框架样式--> <link href="https://www.jb51.net/css/bootstrap.min.css"> <!--图标样式--> <link href="https://cdn.bootcss.com/font-awesome/5.7.2/css/all.min.css"> <!--主要样式--> <link href="https://www.jb51.net/disk/slidercaptcha.css" /> <style> .slidercaptcha { margin: 0 auto; width: 314px; height: 286px; border-radius: 4px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.125); margin-top: 40px; } .slidercaptcha .card-body { padding: 1rem; } .slidercaptcha canvas:first-child { border-radius: 4px; border: 1px solid #e6e8eb; } .slidercaptcha.card .card-header { background-image: none; background-color: rgba(0, 0, 0, 0.03); } .refreshIcon { top: -54px; } </style> </head> <body> <div> <div> <div> <div> <div> <span>请完成安全验证</span> </div> <div><div></div></div> </div> </div> </div> </div> <script src="https://www.jb51.net/js/jquery-1.11.0.min.js" type="text/javascript"></script> <script src="https://www.jb51.net/disk/longbow.slidercaptcha.js"></script> <script> $('#captcha').sliderCaptcha({ repeatIcon: 'fa fa-redo', setSrc: function () { return 'http://images.sdgxgz.com/Pic' + Math.round(Math.random() * 136) + '.jpg'; }, onSuccess: function () { alert('验证通过!'); } }); </script> </body> </html> disk/slidercaptcha.css: body { overflow-x: hidden; } .block { position: absolute; left: 0; top: 0; } .sliderContainer { position: relative; text-align: center; line-height: 40px; background: #f7f9fa; color: #45494c; border-radius: 2px; } .sliderbg { position: absolute; left: 0; right: 0; top: 0; background-color: #f7f9fa; height: 40px; border-radius: 2px; border: 1px solid #e6e8eb; } .sliderContainer_active .slider { top: -1px; border: 1px solid #1991FA; } .sliderContainer_active .sliderMask { border-width: 1px 0 1px 1px; } .sliderContainer_success .slider { top: -1px; border: 1px solid #52CCBA; background-color: #52CCBA !important; } .sliderContainer_success .sliderMask { border: 1px solid #52CCBA; border-width: 1px 0 1px 1px; background-color: #D2F4EF; } .sliderContainer_success .sliderIcon:before { content: "\f00c"; } .sliderContainer_fail .slider { top: -1px; border: 1px solid #f57a7a; background-color: #f57a7a !important; } .sliderContainer_fail .sliderMask { border: 1px solid #f57a7a; background-color: #fce1e1; border-width: 1px 0 1px 1px; } .sliderContainer_fail .sliderIcon:before { content: "\f00d"; } .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; border-radius: 2px; } .slider { position: absolute; top: 0; left: 0; width: 40px; height: 40px; background: #fff; box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); cursor: pointer; transition: background .2s linear; border-radius: 2px; display: flex; align-items: center; justify-content: center; } .slider:hover { background: #1991FA; } .slider:hover .sliderIcon { background-position: 0 -13px; } .sliderText { position: relative; } .sliderIcon { } .refreshIcon { position: absolute; right: 0; top: 0; cursor: pointer; margin: 6px; color: rgba(0,0,0,.25); font-size: 1rem; z-index: 5; transition: color .3s linear; } .refreshIcon:hover { color: #6c757d; } disk/longbow.slidercaptcha.js: (function ($) { 'use strict'; var SliderCaptcha = function (element, options) { this.$element = $(element); this.options = $.extend({}, SliderCaptcha.DEFAULTS, options); this.$element.css({ 'position': 'relative', 'width': this.options.width + 'px', 'margin': '0 auto' }); this.init(); }; SliderCaptcha.VERSION = '1.0'; SliderCaptcha.Author = 'argo@163.com'; SliderCaptcha.DEFAULTS = { width: 280, // canvas宽度 height: 155, // canvas高度 PI: Math.PI, sliderL: 42, // 滑块边长 sliderR: 9, // 滑块半径 offset: 5, // 容错偏差 loadingText: '正在加载中...', failedText: '再试一次', barText: '向右滑动填充拼图', repeatIcon: 'fa fa-repeat', maxLoadCount: 3, localImages: function () { return 'images/Pic' + Math.round(Math.random() * 4) + '.jpg'; } }; function Plugin(option) { return this.each(function () { var $this = $(this); var data = $this.data('lgb.SliderCaptcha'); var options = typeof option =https://www.jb51.net/article/== 'object' && option; if (data && !/reset/.test(option)) return; if (!data) $this.data('lgb.SliderCaptcha', data = new SliderCaptcha(this, options)); if (typeof option =https://www.jb51.net/article/== 'string') data[option](); }); } $.fn.sliderCaptcha = Plugin; $.fn.sliderCaptcha.Constructor = SliderCaptcha; var _proto = SliderCaptcha.prototype; _proto.init = function () { this.initDOM(); this.initImg(); this.bindEvents(); }; _proto.initDOM = function () { var createElement = function (tagName, className) { var elment = document.createElement(tagName); elment.className = className; return elment; }; var createCanvas = function (width, height) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; }; var canvas = createCanvas(this.options.width - 2, this.options.height) // 画布 var block = canvas.cloneNode(true) // 滑块 var sliderContainer = createElement('div', 'sliderContainer'); var refreshIcon = createElement('i', 'refreshIcon ' + this.options.repeatIcon); var sliderMask = createElement('div', 'sliderMask'); var sliderbg = createElement('div', 'sliderbg'); var slider = createElement('div', 'slider'); var sliderIcon = createElement('i', 'fa fa-arrow-right sliderIcon'); var text = createElement('span', 'sliderText'); block.className = 'block' text.innerHTML = this.options.barText; var el = this.$element; el.append($(canvas)); el.append($(refreshIcon)); el.append($(block)); slider.appendChild(sliderIcon); sliderMask.appendChild(slider); sliderContainer.appendChild(sliderbg); sliderContainer.appendChild(sliderMask); sliderContainer.appendChild(text); el.append($(sliderContainer)); Object.assign(this, { canvas, block, sliderContainer: $(sliderContainer), refreshIcon, slider, sliderMask, sliderIcon, text: $(text), canvasCtx: canvas.getContext('2d'), blockCtx: block.getContext('2d') }) }; _proto.initImg = function () { var that = this; var isIE = window.navigator.userAgent.indexOf('Trident') > -1; var L = this.options.sliderL + this.options.sliderR * 2 + 3; // 滑块实际边长 var drawImg = function (ctx, operation) { var l = that.options.sliderL; var r = that.options.sliderR; var PI = that.options.PI; var x = that.x; var y = that.y; ctx.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 = 2 ctx.fillStyle = 'rgba(255, 255, 255, 0.7)' ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)' ctx.stroke() ctx[operation]() ctx.globalCompositeOperation = isIE ? 'xor' : 'overlay' } var getRandomNumberByRange = function (start, end) { return Math.round(Math.random() * (end - start) + start); }; var img = new Image(); img.crossOrigin = "Anonymous"; var loadCount = 0; img.onload = function () { // 随机创建滑块的位置 that.x = getRandomNumberByRange(L + 10, that.options.width - (L + 10)); that.y = getRandomNumberByRange(10 + that.options.sliderR * 2, that.options.height - (L + 10)); drawImg(that.canvasCtx, 'fill'); drawImg(that.blockCtx, 'clip'); that.canvasCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height); that.blockCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height); var y = that.y - that.options.sliderR * 2 - 1; var ImageData = that.blockCtx.getImageData(that.x - 3, y, L, L); that.block.width = L; that.blockCtx.putImageData(ImageData, 0, y); that.text.text(that.text.attr('data-text')); }; img.onerror = function () { loadCount++; if (window.location.protocol =https://www.jb51.net/article/== 'file:') { loadCount = that.options.maxLoadCount; console.error("can't load pic resource file from File protocal. Please try http or https"); } if (loadCount >= that.options.maxLoadCount) { that.text.text('加载失败').addClass('text-danger'); return; } img.src = that.options.localImages(); }; img.setSrc = function () { var src = ''; loadCount = 0; that.text.removeClass('text-danger'); if ($.isFunction(that.options.setSrc)) src = that.options.setSrc(); if (!src || src =https://www.jb51.net/article/== '') src = 'https://picsum.photos/' + that.options.width + 'https://www.jb51.net/' + that.options.height + '/?image=' + Math.round(Math.random() * 20); if (isIE) { // IE浏览器无法通过img.crossOrigin跨域,使用ajax获取图片blob然后转为dataURL显示 var xhr = new XMLHttpRequest() xhr.onloadend = function (e) { var 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(); this.text.attr('data-text', this.options.barText); this.text.text(this.options.loadingText); this.img = img }; _proto.clean = function () { this.canvasCtx.clearRect(0, 0, this.options.width, this.options.height); this.blockCtx.clearRect(0, 0, this.options.width, this.options.height); this.block.width = this.options.width; }; _proto.bindEvents = function () { var that = this; this.$element.on('selectstart', function () { return false; }); $(this.refreshIcon).on('click', function () { that.text.text(that.options.barText); that.reset(); if ($.isFunction(that.options.onRefresh)) that.options.onRefresh.call(that.$element); }); var originX, originY, trail = [], isMouseDown = false var handleDragStart = function (e) { if (that.text.hasClass('text-danger')) return; originX = e.clientX || e.touches[0].clientX; originY = e.clientY || e.touches[0].clientY; isMouseDown = true; }; var handleDragMove = function (e) { if (!isMouseDown) return false; var eventX = e.clientX || e.touches[0].clientX; var eventY = e.clientY || e.touches[0].clientY; var moveX = eventX - originX; var moveY = eventY - originY; if (moveX < 0 || moveX + 40 > that.options.width) return false; that.slider.style.left = (moveX - 1) + 'px'; var blockLeft = (that.options.width - 40 - 20) / (that.options.width - 40) * moveX; that.block.style.left = blockLeft + 'px'; that.sliderContainer.addClass('sliderContainer_active'); that.sliderMask.style.width = (moveX + 4) + 'px'; trail.push(moveY); }; var handleDragEnd = function (e) { if (!isMouseDown) return false isMouseDown = false var eventX = e.clientX || e.changedTouches[0].clientX if (eventX == originX) return false that.sliderContainer.removeClass('sliderContainer_active'); that.trail = trail var { spliced, verified } = that.verify() if (spliced && verified) { that.sliderContainer.addClass('sliderContainer_success'); if ($.isFunction(that.options.onSuccess)) that.options.onSuccess.call(that.$element); } else { that.sliderContainer.addClass('sliderContainer_fail'); if ($.isFunction(that.options.onFail)) that.options.onFail.call(that.$element); setTimeout(() => { that.text.text(that.options.failedText); that.reset(); }, 1000); } }; this.slider.addEventListener('mousedown', handleDragStart); this.slider.addEventListener('touchstart', handleDragStart); document.addEventListener('mousemove', handleDragMove); document.addEventListener('touchmove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); document.addEventListener('touchend', handleDragEnd); document.addEventListener('mousedown', function () { return false; }); document.addEventListener('touchstart', function () { return false; }); }; _proto.verify = function () { var sum = function (x, y) { return x + y; }; var square = function (x) { return x * x; }; var arr = this.trail // 拖动时y轴的移动距离 var average = arr.reduce(sum) / arr.length; var deviations = arr.map(function (x) { return x - average; }); var stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length); var left = parseInt(this.block.style.left); return { spliced: Math.abs(left - this.x) < this.options.offset, verified: stddev !== 0, // 简单验证下拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作 } }; _proto.reset = function () { this.sliderContainer.removeClass('sliderContainer_fail sliderContainer_success'); this.slider.style.left = 0 this.block.style.left = 0 this.sliderMask.style.width = 0 this.clean() this.text.attr('data-text', this.text.text()); this.text.text(this.options.loadingText); this.img.setSrc(); }; })(jQuery);
附录1:需要注意css和js的引用路径;
总结