From d1fd3ca1b49b67077553d6ed85119a40bce6805a Mon Sep 17 00:00:00 2001 From: Yoofff Date: Thu, 16 May 2024 18:25:29 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E9=AA=8C=E8=AF=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/common/captcha.ts | 10 + src/apis/common/type.ts | 26 + .../GiVerify/Verify/VerifyPoints.vue | 297 ++++++++++++ .../GiVerify/Verify/VerifySlide.vue | 457 ++++++++++++++++++ src/components/GiVerify/index.vue | 431 +++++++++++++++++ src/types/components.d.ts | 3 + src/utils/ase.ts | 14 + src/utils/verify.ts | 39 ++ src/views/login/components/phone/index.vue | 26 +- 9 files changed, 1302 insertions(+), 1 deletion(-) create mode 100644 src/components/GiVerify/Verify/VerifyPoints.vue create mode 100644 src/components/GiVerify/Verify/VerifySlide.vue create mode 100644 src/components/GiVerify/index.vue create mode 100644 src/utils/ase.ts create mode 100644 src/utils/verify.ts diff --git a/src/apis/common/captcha.ts b/src/apis/common/captcha.ts index 895720c..35d300d 100644 --- a/src/apis/common/captcha.ts +++ b/src/apis/common/captcha.ts @@ -17,3 +17,13 @@ export function getSmsCaptcha(query: { phone: string }) { export function getEmailCaptcha(query: { email: string }) { return http.get(`${BASE_URL}/mail`, query) } + +/** @desc 获取行为验证码 */ +export function getBehaviorCaptcha(params: any) { + return http.get(`${BASE_URL}/behavior`, {params}); +} + +/** @desc 校验行为验证码 */ +export function checkBehaviorCaptcha(params: any) { + return http.post(`${BASE_URL}/behavior`, params); +} diff --git a/src/apis/common/type.ts b/src/apis/common/type.ts index c332670..27db370 100644 --- a/src/apis/common/type.ts +++ b/src/apis/common/type.ts @@ -18,3 +18,29 @@ export interface DashboardNoticeResp { title: string type: number } + + +/* 行为验证码类型*/ +export interface BehaviorCaptchaReq { + captchaType?: string; + captchaVerification?: string; + clientUid?: string; +} + +export interface BehaviorCaptchaRes { + originalImageBase64: string; + point: { + x: number; + y: number; + }; + jigsawImageBase64: string; + token: string; + secretKey: string; +} + +export interface CheckBehaviorCaptchaRes { + repCode: string; + repMsg: string; +} + + diff --git a/src/components/GiVerify/Verify/VerifyPoints.vue b/src/components/GiVerify/Verify/VerifyPoints.vue new file mode 100644 index 0000000..1bc3ffd --- /dev/null +++ b/src/components/GiVerify/Verify/VerifyPoints.vue @@ -0,0 +1,297 @@ + + + diff --git a/src/components/GiVerify/Verify/VerifySlide.vue b/src/components/GiVerify/Verify/VerifySlide.vue new file mode 100644 index 0000000..73673bc --- /dev/null +++ b/src/components/GiVerify/Verify/VerifySlide.vue @@ -0,0 +1,457 @@ + + + diff --git a/src/components/GiVerify/index.vue b/src/components/GiVerify/index.vue new file mode 100644 index 0000000..3e31c05 --- /dev/null +++ b/src/components/GiVerify/index.vue @@ -0,0 +1,431 @@ + + + + + diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 99dd03f..ac7013b 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -27,6 +27,7 @@ declare module 'vue' { GiTable: typeof import('./../components/GiTable/index.vue')['default'] GiTag: typeof import('./../components/GiTag/index.tsx')['default'] GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default'] + GiVerify: typeof import('./../components/GiVerify/index.vue')['default'] Icon403: typeof import('./../components/icons/Icon403.vue')['default'] Icon404: typeof import('./../components/icons/Icon404.vue')['default'] Icon500: typeof import('./../components/icons/Icon500.vue')['default'] @@ -39,5 +40,7 @@ declare module 'vue' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] TextCopy: typeof import('./../components/TextCopy/index.vue')['default'] + VerifyPoints: typeof import('./../components/GiVerify/Verify/VerifyPoints.vue')['default'] + VerifySlide: typeof import('./../components/GiVerify/Verify/VerifySlide.vue')['default'] } } diff --git a/src/utils/ase.ts b/src/utils/ase.ts new file mode 100644 index 0000000..74633ef --- /dev/null +++ b/src/utils/ase.ts @@ -0,0 +1,14 @@ +import CryptoJS from 'crypto-js'; +/** + * @word 要加密的内容 + * @keyWord String 服务器随机返回的关键字 + * */ +export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') { + const key = CryptoJS.enc.Utf8.parse(keyWord); + const arcs = CryptoJS.enc.Utf8.parse(word); + const encrypted = CryptoJS.AES.encrypt(arcs, key, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }); + return encrypted.toString(); +} \ No newline at end of file diff --git a/src/utils/verify.ts b/src/utils/verify.ts new file mode 100644 index 0000000..901e307 --- /dev/null +++ b/src/utils/verify.ts @@ -0,0 +1,39 @@ +export function resetSize(vm) { + let img_width; + let img_height; + let bar_width; + let bar_height; // 图片的宽度、高度,移动条的宽度、高度 + + const parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth; + const parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight; + if (vm.imgSize.width.indexOf('%') !== -1) { + img_width = `${(parseInt(vm.imgSize.width, 10) / 100) * parentWidth}px`; + } else { + img_width = vm.imgSize.width; + } + + if (vm.imgSize.height.indexOf('%') !== -1) { + img_height = `${(parseInt(vm.imgSize.height, 10) / 100) * parentHeight}px`; + } else { + img_height = vm.imgSize.height; + } + + if (vm.barSize.width.indexOf('%') !== -1) { + bar_width = `${(parseInt(vm.barSize.width, 10) / 100) * parentWidth}px`; + } else { + bar_width = vm.barSize.width; + } + + if (vm.barSize.height.indexOf('%') !== -1) { + bar_height = `${(parseInt(vm.barSize.height, 10) / 100) * parentHeight}px`; + } else { + bar_height = vm.barSize.height; + } + + return { + imgWidth: img_width, + imgHeight: img_height, + barWidth: bar_width, + barHeight: bar_height, + }; +} diff --git a/src/views/login/components/phone/index.vue b/src/views/login/components/phone/index.vue index 6f97ae8..c8a8101 100644 --- a/src/views/login/components/phone/index.vue +++ b/src/views/login/components/phone/index.vue @@ -18,7 +18,7 @@ :loading="captchaLoading" :disabled="captchaDisable" size="large" - @click="onCaptcha" + @click="handleOpenBehaviorCaptcha" > {{ captchaBtnName }} @@ -30,6 +30,13 @@ + diff --git a/src/components/GiVerify/Verify/VerifySlide.vue b/src/components/GiVerify/Verify/VerifySlide.vue index 73673bc..2212fa1 100644 --- a/src/components/GiVerify/Verify/VerifySlide.vue +++ b/src/components/GiVerify/Verify/VerifySlide.vue @@ -3,20 +3,22 @@
+ > + +
import { computed, + getCurrentInstance, + nextTick, onMounted, reactive, ref, - watch, - nextTick, toRefs, - getCurrentInstance, -} from 'vue'; + watch +} from 'vue' import { checkBehaviorCaptcha, - getBehaviorCaptcha, -} from '@/apis/common/captcha'; + getBehaviorCaptcha +} from '@/apis/common/captcha' import { aesEncrypt } from '@/utils/ase' import { resetSize } from '@/utils/verify' @@ -113,311 +115,311 @@ export default { name: 'VerifySlide', props: { captchaType: { - type: String, + type: String }, type: { type: String, - default: '1', + default: '1' }, // 弹出式pop,固定fixed mode: { type: String, - default: 'fixed', + default: 'fixed' }, vSpace: { type: Number, - default: 5, + default: 5 }, explain: { type: String, - default: '向右滑动完成验证', + default: '向右滑动完成验证' }, imgSize: { type: Object, default() { return { width: '310px', - height: '155px', - }; - }, + height: '155px' + } + } }, blockSize: { type: Object, default() { return { width: '50px', - height: '50px', - }; - }, + height: '50px' + } + } }, barSize: { type: Object, default() { return { width: '310px', - height: '40px', - }; - }, - }, + height: '40px' + } + } + } }, setup(props) { - const { mode, captchaType, type, blockSize, explain } = toRefs(props); - const { proxy } = getCurrentInstance(); - const secretKey = ref(); // 后端返回的ase加密秘钥 - const passFlag = ref(); // 是否通过的标识 - const backImgBase = ref(); // 验证码背景图片 - const blockBackImgBase = ref(); // 验证滑块的背景图片 - const backToken = ref(); // 后端返回的唯一token值 - const startMoveTime = ref(); // 移动开始的时间 - const endMovetime = ref(); // 移动结束的时间 - const tipsBackColor = ref(); // 提示词的背景颜色 - const tipWords = ref(); - const text = ref(); - const finishText = ref(); + const { mode, captchaType, type, blockSize, explain } = toRefs(props) + const { proxy } = getCurrentInstance() + const secretKey = ref() // 后端返回的ase加密秘钥 + const passFlag = ref() // 是否通过的标识 + const backImgBase = ref() // 验证码背景图片 + const blockBackImgBase = ref() // 验证滑块的背景图片 + const backToken = ref() // 后端返回的唯一token值 + const startMoveTime = ref() // 移动开始的时间 + const endMovetime = ref() // 移动结束的时间 + const tipsBackColor = ref() // 提示词的背景颜色 + const tipWords = ref() + const text = ref() + const finishText = ref() const setSize = reactive({ imgHeight: 0, imgWidth: 0, barHeight: 0, - barWidth: 0, - }); - const top = ref(0); - const left = ref(0); - const moveBlockLeft = ref(); - const leftBarWidth = ref(); + barWidth: 0 + }) + const top = ref(0) + const left = ref(0) + const moveBlockLeft = ref() + const leftBarWidth = ref() // 移动中样式 - const moveBlockBackgroundColor = ref(); - const leftBarBorderColor = ref('#ddd'); - const iconColor = ref(); - const iconClass = ref('icon-right'); - const status = ref(false); // 鼠标状态 - const isEnd = ref(false); // 是够验证完成 - const showRefresh = ref(true); - const transitionLeft = ref(''); - const transitionWidth = ref(''); - const startLeft = ref(0); + const moveBlockBackgroundColor = ref() + const leftBarBorderColor = ref('#ddd') + const iconColor = ref() + const iconClass = ref('icon-right') + const status = ref(false) // 鼠标状态 + const isEnd = ref(false) // 是够验证完成 + const showRefresh = ref(true) + const transitionLeft = ref('') + const transitionWidth = ref('') + const startLeft = ref(0) // 请求背景图片和验证图片 - function getPictrue() { + function getPicture() { const data = { - captchaType: captchaType.value, - }; + captchaType: captchaType.value + } getBehaviorCaptcha(data).then((res) => { - backImgBase.value = res.data.originalImageBase64; - blockBackImgBase.value = res.data.jigsawImageBase64; - backToken.value = res.data.token; - secretKey.value = res.data.secretKey; - }); + backImgBase.value = res.data.originalImageBase64 + blockBackImgBase.value = res.data.jigsawImageBase64 + backToken.value = res.data.token + secretKey.value = res.data.secretKey + }) } const barArea = computed(() => { - return proxy.$el.querySelector('.verify-bar-area'); - }); + return proxy.$el.querySelector('.verify-bar-area') + }) // 鼠标移动 function move(e) { - e = e || window.event; + e = e || window.event if (status.value && isEnd.value === false) { - let x; + let x if (!e.touches) { // 兼容PC端 - x = e.clientX; + x = e.clientX } else { // 兼容移动端 - x = e.touches[0].pageX; + x = e.touches[0].pageX } - const bar_area_left = barArea.value.getBoundingClientRect().left; - let move_block_left = x - bar_area_left; // 小方块相对于父元素的left值 + const bar_area_left = barArea.value.getBoundingClientRect().left + let move_block_left = x - bar_area_left // 小方块相对于父元素的left值 if ( - move_block_left >= - barArea.value.offsetWidth - - parseInt(blockSize.value.width, 10) / 2 - 2 + move_block_left + >= barArea.value.offsetWidth + - Number.parseInt(blockSize.value.width, 10) / 2 - 2 ) { - move_block_left = - barArea.value.offsetWidth - - parseInt(blockSize.value.width, 10) / 2 - 2; + move_block_left + = barArea.value.offsetWidth + - Number.parseInt(blockSize.value.width, 10) / 2 - 2 } if (move_block_left <= 0) { - move_block_left = parseInt(blockSize.value.width, 10) / 2; + move_block_left = Number.parseInt(blockSize.value.width, 10) / 2 } // 拖动后小方块的left值 - moveBlockLeft.value = `${move_block_left - startLeft.value}px`; - leftBarWidth.value = `${move_block_left - startLeft.value}px`; + moveBlockLeft.value = `${move_block_left - startLeft.value}px` + leftBarWidth.value = `${move_block_left - startLeft.value}px` } } const refresh = () => { - showRefresh.value = true; - finishText.value = ''; + showRefresh.value = true + finishText.value = '' - transitionLeft.value = 'left .3s'; - moveBlockLeft.value = 0; + transitionLeft.value = 'left .3s' + moveBlockLeft.value = 0 - leftBarWidth.value = undefined; - transitionWidth.value = 'width .3s'; + leftBarWidth.value = undefined + transitionWidth.value = 'width .3s' - leftBarBorderColor.value = '#ddd'; - moveBlockBackgroundColor.value = '#fff'; - iconColor.value = '#000'; - iconClass.value = 'icon-right'; - isEnd.value = false; + leftBarBorderColor.value = '#ddd' + moveBlockBackgroundColor.value = '#fff' + iconColor.value = '#000' + iconClass.value = 'icon-right' + isEnd.value = false - getPictrue(); + getPicture() setTimeout(() => { - transitionWidth.value = ''; - transitionLeft.value = ''; - text.value = explain.value; - }, 300); - }; + transitionWidth.value = '' + transitionLeft.value = '' + text.value = explain.value + }, 300) + } // 鼠标松开 function end() { - endMovetime.value = +new Date(); + endMovetime.value = +new Date() // 判断是否重合 if (status.value && isEnd.value === false) { - let moveLeftDistance = parseInt( - (moveBlockLeft.value || '').replace('px', ''), - 10, - ); - moveLeftDistance = - (moveLeftDistance * 310) / parseInt(setSize.imgWidth+'', 10); + let moveLeftDistance = Number.parseInt( + (moveBlockLeft.value || '').replace('px', ''), + 10 + ) + moveLeftDistance + = (moveLeftDistance * 310) / Number.parseInt(`${setSize.imgWidth}`, 10) const data = { captchaType: captchaType.value, pointJson: secretKey.value - ? aesEncrypt( - JSON.stringify({ x: moveLeftDistance, y: 5.0 }), - secretKey.value, - ) - : JSON.stringify({ x: moveLeftDistance, y: 5.0 }), - token: backToken.value, - }; + ? aesEncrypt( + JSON.stringify({ x: moveLeftDistance, y: 5.0 }), + secretKey.value + ) + : JSON.stringify({ x: moveLeftDistance, y: 5.0 }), + token: backToken.value + } checkBehaviorCaptcha(data).then((res) => { if (res.success && res.data.repCode === '0000') { - moveBlockBackgroundColor.value = '#5cb85c'; - leftBarBorderColor.value = '#5cb85c'; - iconColor.value = '#fff'; - iconClass.value = 'icon-check'; - showRefresh.value = false; - isEnd.value = true; + moveBlockBackgroundColor.value = '#5cb85c' + leftBarBorderColor.value = '#5cb85c' + iconColor.value = '#fff' + iconClass.value = 'icon-check' + showRefresh.value = false + isEnd.value = true if (mode.value === 'pop') { setTimeout(() => { - proxy.$parent.clickShow = false; - refresh(); - }, 1500); + proxy.$parent.clickShow = false + refresh() + }, 1500) } - passFlag.value = true; + passFlag.value = true tipWords.value = `${( - (endMovetime.value - startMoveTime.value) / - 1000 - ).toFixed(2)}s验证成功`; + (endMovetime.value - startMoveTime.value) + / 1000 + ).toFixed(2)}s验证成功` const captchaVerification = secretKey.value - ? aesEncrypt( + ? aesEncrypt( `${backToken.value}---${JSON.stringify({ x: moveLeftDistance, - y: 5.0, + y: 5.0 })}`, - secretKey.value, - ) - : `${backToken.value}---${JSON.stringify({ + secretKey.value + ) + : `${backToken.value}---${JSON.stringify({ x: moveLeftDistance, - y: 5.0, - })}`; + y: 5.0 + })}` setTimeout(() => { - tipWords.value = ''; - proxy.$parent.closeBox(); - proxy.$parent.$emit('success', { captchaVerification }); - }, 1000); + tipWords.value = '' + proxy.$parent.closeBox() + proxy.$parent.$emit('success', { captchaVerification }) + }, 1000) } else { - moveBlockBackgroundColor.value = '#d9534f'; - leftBarBorderColor.value = '#d9534f'; - iconColor.value = '#fff'; - iconClass.value = 'icon-close'; - passFlag.value = false; - setTimeout(function () { - refresh(); - }, 1000); - proxy.$parent.$emit('error', proxy); - tipWords.value = res.data.repMsg; + moveBlockBackgroundColor.value = '#d9534f' + leftBarBorderColor.value = '#d9534f' + iconColor.value = '#fff' + iconClass.value = 'icon-close' + passFlag.value = false + setTimeout(() => { + refresh() + }, 1000) + proxy.$parent.$emit('error', proxy) + tipWords.value = res.data.repMsg setTimeout(() => { - tipWords.value = ''; - }, 1000); + tipWords.value = '' + }, 1000) } - }); - status.value = false; + }) + status.value = false } } function init() { - text.value = explain.value; - getPictrue(); + text.value = explain.value + getPicture() nextTick(() => { - const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy); - setSize.imgHeight = imgHeight; - setSize.imgWidth = imgWidth; - setSize.barHeight = barHeight; - setSize.barWidth = barWidth; - proxy.$parent.$emit('ready', proxy); - }); + const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy) + setSize.imgHeight = imgHeight + setSize.imgWidth = imgWidth + setSize.barHeight = barHeight + setSize.barWidth = barWidth + proxy.$parent.$emit('ready', proxy) + }) - window.removeEventListener('touchmove', function (e) { - move(e); - }); - window.removeEventListener('mousemove', function (e) { - move(e); - }); + window.removeEventListener('touchmove', (e) => { + move(e) + }) + window.removeEventListener('mousemove', (e) => { + move(e) + }) // 鼠标松开 - window.removeEventListener('touchend', function () { - end(); - }); - window.removeEventListener('mouseup', function () { - end(); - }); + window.removeEventListener('touchend', () => { + end() + }) + window.removeEventListener('mouseup', () => { + end() + }) - window.addEventListener('touchmove', function (e) { - move(e); - }); - window.addEventListener('mousemove', function (e) { - move(e); - }); + window.addEventListener('touchmove', (e) => { + move(e) + }) + window.addEventListener('mousemove', (e) => { + move(e) + }) // 鼠标松开 - window.addEventListener('touchend', function () { - end(); - }); - window.addEventListener('mouseup', function () { - end(); - }); + window.addEventListener('touchend', () => { + end() + }) + window.addEventListener('mouseup', () => { + end() + }) } watch(type, () => { - init(); - }); + init() + }) onMounted(() => { // 禁止拖拽 - init(); + init() proxy.$el.onselectstart = function () { - return false; - }; - }); + return false + } + }) // 鼠标按下 function start(e) { - e = e || window.event; - let x; + e = e || window.event + let x if (!e.touches) { // 兼容PC端 - x = e.clientX; + x = e.clientX } else { // 兼容移动端 - x = e.touches[0].pageX; + x = e.touches[0].pageX } startLeft.value = Math.floor( - x - barArea.value.getBoundingClientRect().left, - ); - startMoveTime.value = +new Date(); // 开始滑动的时间 + x - barArea.value.getBoundingClientRect().left + ) + startMoveTime.value = +new Date() // 开始滑动的时间 if (isEnd.value === false) { - text.value = ''; - moveBlockBackgroundColor.value = '#337ab7'; - leftBarBorderColor.value = '#337AB7'; - iconColor.value = '#fff'; - e.stopPropagation(); - status.value = true; + text.value = '' + moveBlockBackgroundColor.value = '#337ab7' + leftBarBorderColor.value = '#337AB7' + iconColor.value = '#fff' + e.stopPropagation() + status.value = true } } @@ -450,8 +452,8 @@ export default { transitionWidth, barArea, refresh, - start, - }; - }, -}; + start + } + } +} diff --git a/src/components/GiVerify/index.vue b/src/components/GiVerify/index.vue index 3e31c05..25c13cf 100644 --- a/src/components/GiVerify/index.vue +++ b/src/components/GiVerify/index.vue @@ -2,7 +2,7 @@
请完成安全验证 @@ -36,99 +36,97 @@