From 296ac82a4c3a3b51d728ade69f2c2a8cb538f0b2 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 12 Mar 2019 21:05:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8A=9F=E8=83=BD=EF=BC=9A?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=99=BB=E5=BD=95=E9=AA=8C=E8=AF=81=E7=A0=81?= =?UTF-8?q?=20closed=20#IRWNP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #Issue https://gitee.com/LongbowEnterprise/dashboard/issues?id=IRWNP --- Bootstrap.Admin/Views/Account/Login.cshtml | 9 + Bootstrap.Admin/wwwroot/css/login.css | 28 ++ Bootstrap.Admin/wwwroot/js/login.js | 17 + .../lib/captcha/longbow.slidercaptcha.js | 295 ++++++++++++++++++ .../wwwroot/lib/captcha/slidercaptcha.css | 130 ++++++++ 5 files changed, 479 insertions(+) create mode 100644 Bootstrap.Admin/wwwroot/lib/captcha/longbow.slidercaptcha.js create mode 100644 Bootstrap.Admin/wwwroot/lib/captcha/slidercaptcha.css diff --git a/Bootstrap.Admin/Views/Account/Login.cshtml b/Bootstrap.Admin/Views/Account/Login.cshtml index 89db252c..a32bfaeb 100644 --- a/Bootstrap.Admin/Views/Account/Login.cshtml +++ b/Bootstrap.Admin/Views/Account/Login.cshtml @@ -14,6 +14,7 @@ + @@ -35,6 +36,7 @@ + @@ -73,6 +75,13 @@ 申请账号 忘记密码 +
+
+ 请完成安全验证 + +
+
+
diff --git a/Bootstrap.Admin/wwwroot/css/login.css b/Bootstrap.Admin/wwwroot/css/login.css index bc51545f..6b3341ad 100644 --- a/Bootstrap.Admin/wwwroot/css/login.css +++ b/Bootstrap.Admin/wwwroot/css/login.css @@ -19,6 +19,7 @@ .login-wrap .rememberPwd { cursor: pointer; color: #333; + margin-left: 2px; } .login-wrap .rememberPwd i { @@ -53,3 +54,30 @@ .form-control { border-color: #1ca0e9; } + +.slidercaptcha { + display: none; + position: relative; + bottom: 254px; + background-color: #00adec; + width: 310px; + height: 280px; + border-radius: 4px; + left: -10px; + box-shadow: 0 0 10px #fff; +} + + .slidercaptcha canvas:first-child { + border-radius: 4px; + border: solid 1px #0076c9; + } + + .slidercaptcha .close { + color: #fff; + margin-top: -2px; + } + + .slidercaptcha.card .card-header { + background-image: none; + background-color: rgba(0, 0, 0, 0.03); + } diff --git a/Bootstrap.Admin/wwwroot/js/login.js b/Bootstrap.Admin/wwwroot/js/login.js index 7a22d06e..26eb275b 100644 --- a/Bootstrap.Admin/wwwroot/js/login.js +++ b/Bootstrap.Admin/wwwroot/js/login.js @@ -51,4 +51,21 @@ $rem.val('false'); } }); + + var $captcha = $('.slidercaptcha'); + $('.slidercaptcha .close').on('click', function() { + $captcha.removeClass('d-block'); + }); + + $('button[type="submit"]').on('click', function(e){ + if ($.browser.versions.mobile) return true; + $captcha.addClass('d-block'); + return false; + }); + + $('#captcha').sliderCaptcha({ + onSuccess: function () { + $('form').submit(); + } + }); }); \ No newline at end of file diff --git a/Bootstrap.Admin/wwwroot/lib/captcha/longbow.slidercaptcha.js b/Bootstrap.Admin/wwwroot/lib/captcha/longbow.slidercaptcha.js new file mode 100644 index 00000000..c1c5028d --- /dev/null +++ b/Bootstrap.Admin/wwwroot/lib/captcha/longbow.slidercaptcha.js @@ -0,0 +1,295 @@ +(function ($) { + 'use strict'; + + var isIE = window.navigator.userAgent.indexOf('Trident') > -1; + var L = 63; + + function getRandomNumberByRange(start, end) { + return Math.round(Math.random() * (end - start) + start); + } + + function createCanvas(width, height) { + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + } + + function createImg(w, h, onload) { + var img = new Image(); + img.crossOrigin = "Anonymous"; + img.onload = onload; + img.onerror = function () { + img.setSrc(getRandomImgSrc(w, h)); + } + + img.setSrc = function (src) { + 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(getRandomImgSrc(w, h)); + return img; + } + + function createElement(tagName, className) { + var elment = document.createElement(tagName); + elment.className = className; + return elment; + } + + function addClass(tag, className) { + tag.classList.add(className); + } + + function removeClass(tag, className) { + tag.classList.remove(className); + } + + function getRandomImgSrc(w, h) { + return '//picsum.photos/' + w + '/' + h + '/?image=' + getRandomNumberByRange(0, 1084); + } + + function draw(ctx, x, y, l, r, PI, operation) { + 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' + } + + function sum(x, y) { + return x + y + } + + function square(x) { + return x * x + } + + 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' }); + + var L = this.options.sliderL + this.options.sliderR * 2 + 3; // 滑块实际边长 + }; + + SliderCaptcha.VERSION = '1.0'; + SliderCaptcha.Author = 'argo@163.com'; + SliderCaptcha.DEFAULTS = { + width: 280, // canvas宽度 + height: 155, // canvas高度 + PI: Math.PI, + sliderL: 42, // 滑块边长 + sliderR: 9, // 滑块半径 + loadingText: '正在加载中...', + failedText: '再试一次', + barText: '向右滑动填充拼图' + }; + + function Plugin(option) { + return this.each(function () { + var $this = $(this); + var data = $this.data('lgb.SliderCaptcha'); + var options = typeof option === 'object' && option; + + if (!data && /init|reset/.test(option)) return; + if (!data) { + $this.data('lgb.SliderCaptcha', data = new SliderCaptcha(this, options)); + data.init(); + } + if (typeof option === '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 canvas = createCanvas(this.options.width - 2, this.options.height) // 画布 + var block = canvas.cloneNode(true) // 滑块 + var sliderContainer = createElement('div', 'sliderContainer'); + var refreshIcon = createElement('i', 'refreshIcon fa fa-repeat'); + 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, + refreshIcon, + slider, + sliderMask, + sliderIcon, + text: $(text), + canvasCtx: canvas.getContext('2d'), + blockCtx: block.getContext('2d') + }) + }; + + _proto.initImg = function () { + var that = this; + var img = createImg(this.options.width, this.options.height, function () { + that.draw(); + 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')); + }) + this.text.attr('data-text', this.options.barText); + this.text.text(this.options.loadingText); + this.img = img + }; + + _proto.draw = function () { + // 随机创建滑块的位置 + this.x = getRandomNumberByRange(L + 10, this.options.width - (L + 10)) + this.y = getRandomNumberByRange(10 + this.options.sliderR * 2, this.options.height - (L + 10)) + draw(this.canvasCtx, this.x, this.y, this.options.sliderL, this.options.sliderR, this.options.PI, 'fill') + draw(this.blockCtx, this.x, this.y, this.options.sliderL, this.options.sliderR, this.options.PI, 'clip') + }; + + _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.reset(); + typeof that.onRefresh === 'function' && that.onRefresh() + }); + + var originX, originY, trail = [], + isMouseDown = false + + var handleDragStart = function (e) { + 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 + 'px'; + var blockLeft = (that.options.width - 40 - 20) / (that.options.width - 40) * moveX; + that.block.style.left = blockLeft + 'px'; + + addClass(that.sliderContainer, '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 + removeClass(that.sliderContainer, 'sliderContainer_active') + that.trail = trail + var { + spliced, + verified + } = that.verify() + if (spliced && verified) { + addClass(that.sliderContainer, 'sliderContainer_success') + if ($.isFunction(that.options.onSuccess)) that.options.onSuccess.call(that.$element); + } else { + addClass(that.sliderContainer, '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); + }; + + _proto.verify = function () { + var arr = this.trail // 拖动时y轴的移动距离 + var average = arr.reduce(sum) / arr.length; + var deviations = arr.map(x => 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) < 10, + verified: stddev !== 0, // 简单验证下拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作 + } + }; + + _proto.reset = function () { + this.sliderContainer.className = 'sliderContainer' + 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(getRandomImgSrc(this.options.width, this.options.height)); + }; +})(jQuery); \ No newline at end of file diff --git a/Bootstrap.Admin/wwwroot/lib/captcha/slidercaptcha.css b/Bootstrap.Admin/wwwroot/lib/captcha/slidercaptcha.css new file mode 100644 index 00000000..0d62bc04 --- /dev/null +++ b/Bootstrap.Admin/wwwroot/lib/captcha/slidercaptcha.css @@ -0,0 +1,130 @@ +body { + overflow: 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; +} + +.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: #007ec7; + font-size: 1rem; + z-index: 5; + transition: color .3s linear; +} + +.refreshIcon:hover { + color: #05639a; +}