由于步驟太多,字數太多,廢話也太多,所以前后端分開講了,后端文章請看:
超詳細 anji-captcha滑塊驗證springboot+uniapp微信小程序前后端組合https://blog.csdn.net/new_public/article/details/149116742
anji-captcha開源項目地址:https://github.com/anji-plus/captcha
anji-captcha開源文檔地址:在線體驗暫時下線 !!! | AJ-Captcha
寫完后端代碼,開始寫前端,首選肯定又是一頓各種網上查資料搬磚,發現基本清一色用了anji-captcha開源文檔介紹的【Verify】組件,這組件是anji-captcha開源項目里面有的,在view文件夾下面,選擇自己對應的前端類型,里面有下面幾種類型。
比如:/view/vue/src/components/verifition/Verify?
?不過我沒去看,是自己寫了一個,畢竟只有自己寫的才是最適合自己的。
效果如下:
每次滑動有廣告語出現,比如視頻里的,【大怨種】【純牛馬】。
獲取次數和驗證次數超限,會顯示提示等。
組件代碼
<template><modal ref="$captchaModal" :width="modalWidth" :padding="modalPadding"><template #body><view class="captcha-modal-content" :style="{ height: 'calc(' + slideImageHeight + 'px + ' + slideBlockWidth + 'px + 12rpx)' }"><template v-if="sliderCaptchaBackBase64"><image class="captcha-back-image":style="{ width: slideImageWidth + 'px', height: slideImageHeight + 'px' }":src="sliderCaptchaBackBase64" /><image class="captcha-slider-image" :src="sliderCaptchaBlockBase64":style="{ width: slideBlockWidth + 'px', height: slideImageHeight + 'px', transform: 'translate3d(' + lastLeft + 'px,0,0)' }" /></template><image v-else src="/static/images/svg/null_data.svg" class="captcha-back-image" style="height: 300rpx;"/><view class="slide-parent" :style="{ width: slideImageWidth + 'px', height: slideBlockWidth + 'px',backgroundColor: sliderCaptchaBackBase64 ? 'rgb(233, 233, 233)' : 'transparent' }"><template v-if="sliderCaptchaBackBase64"><view class="slide-tip tui-text-flashover">拖動滑塊驗證</view><view class="slide-cover" :style="{ width: lastLeft + 'px' }"><view class="slide-cover-content" :style="{ width: slideImageWidth + 'px' }">{{ slideCoverText }}</view></view><view class="slide-view" :style="{ width: slideBlockWidth + 'px', height: slideBlockWidth + 'px', transform: 'translate3d(' + lastLeft + 'px,0,0)' }"@touchstart="touchstart" @touchmove="touchmove" @touchend="touchend"><tui-icon name="up" :size="19" style="transform: rotate(90deg);" /></view></template><view v-else class="tui-flex-center" style="height: 100%;":class="{ 'tui-underline': captChaGetRepCode !== '6201' }"@click="delayedGenerateSlideCaptchaImage">{{ errorMsg }}</view></view></view></template></modal>
</template>
<script>// 這里用到util是防抖動方法和四舍五入方法,自己寫一個吧,或者問api給一個import util from '@/utils/util'// npm i crypto-js -s 引入,這里用的是4.2.0版本import CryptoJS from 'crypto-js/crypto-js'export default {data () {return {// 滑塊寬高度 pxslideBlockWidth: 47,// 背景圖寬度 pxslideImageWidth: 310,// 背景圖高度 pxslideImageHeight: 155,// 彈窗占據屏幕 90%modalWidth: 90,// 彈窗內部左右邊距20pxmodalLRpadding: 20,// 縮放比率,不同屏幕的手機,必須適配scaleRatio: 0,// 滑動起始X位置 pxstartX: 0,// 滑動停止位置,也就是距離起始位置長度 pxlastLeft: 0,// 后端返回的驗證碼參數captchaOption: {},// 父頁面參數parentOption: {},// 獲取滑塊驗證碼返回的結果碼,用來顯示錯誤信息的captChaGetRepCode: void (0),// 廣告語,每次隨機取slideCoverTextList: ['大怨種', '純牛馬', '肝吧', '擠上牛馬傳送帶', '這就是命啊']}},created () {// 初始化背景圖和滑塊縮放this.handlerCaptchaScale()// 手動獲取滑塊驗證碼的方法,加上防抖動,350毫秒this.delayedGenerateSlideCaptchaImage = util.debounce(this.delayedGenerateSlideCaptchaImage, 350)},computed: {modalPadding () {return `40rpx ${this.modalLRpadding}px 20rpx ${this.modalLRpadding}px`},// 廣告語,隨機取,為了能隨機,故跟startX綁定上了slideCoverText () {const initIndex = Math.floor(Math.random() * this.slideCoverTextList.length)const startXStr = String(this.startX)const finalIndex = initIndex + (startXStr.length > 1 ? Number(startXStr.substring(startXStr.length - 1)) : this.startX)return this.slideCoverTextList[finalIndex > this.slideCoverTextList.length - 1 ? initIndex : finalIndex]},// 背景圖base64sliderCaptchaBackBase64 () {const { originalImageBase64 } = this.captchaOptionif (!originalImageBase64) {return ''}return 'data:image/png;base64,' + originalImageBase64},// 滑塊base64sliderCaptchaBlockBase64 () {const { jigsawImageBase64 } = this.captchaOptionif (!jigsawImageBase64) {return ''}return 'data:image/png;base64,' + jigsawImageBase64},errorMsg () {if (this.captChaGetRepCode === '6201') {return '獲取圖形驗證碼頻繁,請稍后再試'} else if (this.captChaGetRepCode !== '0000') {return '啊哦,加載失敗了,點擊這里刷新'}return ''}},methods: {// 顯示滑塊驗證碼彈窗,父頁面通過ref通用show (option = {}) {this.resetSlideCaptcha()this.parentOption = optionthis.generateSlideCaptchaImage().then(_ => {this.$nextTick(() => {// 調用彈窗組件方法,顯示彈窗this.$refs.$captchaModal.show({title: option.title || '驗證',maxHeight: 800,hideCancel: true,confirmText: '關閉',// 彈窗關閉按鈕點擊回調callback: _ => {// 如果父頁面有關閉回調,則這里調用this.parentOption.closeCallback && this.parentOption.closeCallback()}})})})},close () {this.resetSlideCaptcha()// 關閉彈窗this.$refs.$captchaModal.onClose()},// 初始化縮放比率,以及設置滑塊背景和滑塊寬高handlerCaptchaScale () {const { windowWidth } = uni.getSystemInfoSync()const modalWidthPx = windowWidth * (this.modalWidth / 100)const modalLRpaddingSum = this.modalLRpadding * 2const modalInternalWidth = modalWidthPx - modalLRpaddingSumconst onePercentagePx = windowWidth * 0.01if (modalInternalWidth > this.slideImageWidth) {// 如果屏幕寬度大于初始化滑塊背景圖寬度,則不縮放this.modalWidth = util.toFixed((this.slideImageWidth + modalLRpaddingSum) / onePercentagePx, 2)} else if (modalInternalWidth < this.slideImageWidth) {// 否則縮放滑塊背景圖和滑塊this.scaleRatio = modalInternalWidth / this.slideImageWidththis.slideImageWidth = modalInternalWidththis.slideImageHeight = this.slideImageHeight * this.scaleRatiothis.slideBlockWidth = this.slideBlockWidth * this.scaleRatio}},// 手動重新獲取delayedGenerateSlideCaptchaImage () {if (this.captChaGetRepCode === '6201') {return}this.generateSlideCaptchaImage()},// 獲取滑塊驗證碼generateSlideCaptchaImage () {return new Promise((resolve, reject) => {// request 基于uni.request封裝,總之就是調獲取驗證碼接口request('/captcha/get', 'POST', { 'captchaType' : 'blockPuzzle' }, true).then(({ repData = {}, repCode }) => {this.captChaGetRepCode = repCodethis.captchaOption = repData}).finally(() => {resolve()})})},// 按中滑塊,記錄起始位置touchstart (e) {const touch = e.touches[0] || e.changedTouches[0]this.startX = touch.clientX},// 開始滑動,計算當前滑動位置touchmove (e) {const touch = e.touches[0] || e.changedTouches[0]const pageX = touch.clientXconst width = this.slideImageWidth - this.slideBlockWidth - 1let left = pageX - this.startXleft = left < 0 ? 0 : (left >= width ? width : left)this.lastLeft = left},// 滑動結束(松手),驗證touchend () {/*** callback 父頁面驗證結果回調* verifyCaptchaCallback 整個驗證由父頁面處理* verifyApi 驗證后端接口* * callback 和 verifyCaptchaCallback 二選一* 選callback,verifyApi必傳,讓這個這個組件調用驗證接口*/const { callback, verifyCaptchaCallback, verifyApi } = this.parentOptionif (callback || verifyCaptchaCallback) {if (verifyCaptchaCallback) {verifyCaptchaCallback(this.captchaOption).then(flag => {if (flag) {this.close()} else {this.resetSlideCaptcha(true)}})} else {this.verifyCaptcha(verifyApi).then(res => {if (res) {// 驗證成功,記錄本次驗證碼的aes密匙,給后面后端二次驗證使用res.secretKey = this.captchaOption.secretKeythis.close()} else {// 驗證失敗重新獲取this.resetSlideCaptcha(true)}callback(res)})}} else {this.resetSlideCaptcha(true)}},// 重置resetSlideCaptcha (generateFlag) {this.lastLeft = 0this.startX = 0generateFlag && this.generateSlideCaptchaImage()},// 驗證滑動是否正確verifyCaptcha (verifyApi) {return new Promise((resolve, reject) => {const originalPointJson = this.generatePointJson()const param = {captchaType: 'blockPuzzle',token: this.captchaOption.token,pointJson: this.pointEncrypted(originalPointJson)}// request 基于uni.request封裝,總之就是調驗證接口request(verifyApi || '/captcha/check', 'POST', param, true).then(res => {const { repCode, repData = {}, repMsg } = resif (repCode === '0000' && repData.result) {resolve({ ...repData, originalPointJson })} else {// toast 等同 uni.showToast()toast(repMsg || '驗證失敗')resolve(false)}}).catch(_ => {resolve(false)})})},// 生成當前滑動完成的坐標點,加密的原文generatePointJson () {const x = this.scaleRatio > 0 ? ((this.lastLeft - 1) / this.scaleRatio) : this.lastLeft - 1return JSON.stringify({x,y:0})},// aes加密坐標點pointEncrypted (originalPointJson) {const key = CryptoJS.enc.Utf8.parse(this.captchaOption.secretKey)const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(originalPointJson),key,{mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7})return encrypted.toString()},// 生成后端二次驗證的密文captchaVerificationEncrypted ({ originalPointJson, token, secretKey }) {const key = CryptoJS.enc.Utf8.parse(secretKey)const dataToEncrypt = token + '---' + originalPointJsonconst encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(dataToEncrypt),key,{mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7})return encrypted.toString()}}}
</script>
<style scoped lang="scss">.captcha-modal-content {position: relative;.captcha-back-image {width: 100%;border-radius: 10rpx;}.captcha-slider-image {position: absolute;left: 0;top: 0;z-index: 1;}.slide-parent {width: 100%;background-color: rgb(233, 233, 233);position: relative;padding: 1px 0;.slide-tip {position: absolute;top: 0;left: 0;right: 0;bottom: 0;color: #616161;font-size: 30rpx;display: flex;align-items: center;justify-content: center;}.slide-cover {width: 0;height: 100%;position: absolute;left: 0;top: 0;z-index: 1;display: flex;align-items: center;justify-content: center;overflow: hidden;.slide-cover-content {height: 100%;color: #FFF;background-color: #0081ff;font-size: 30rpx;display: flex;align-items: center;justify-content: center;position: absolute;left: 0;top: 0;}}.slide-view {position: absolute;left: 1px;z-index: 2;background: #FFF;display: flex;align-items: center;justify-content: center;}}}.tui-text-flashover {background: -webkit-gradient(linear, left top, right top, color-stop(0, #444), color-stop(.4, #444), color-stop(.5, white), color-stop(.6, #444), color-stop(1, #444));-webkit-background-clip: text !important;-webkit-text-fill-color: transparent !important;-webkit-animation: animate 1.8s infinite;}.tui-flex-center {display: flex;align-items: center;justify-content: center;}.tui-underline {text-decoration: underline;}@-webkit-keyframes animate {from {background-position: -90rpx;}to {background-position: 90rpx;}}@keyframes animate {from {background-position: -90rpx;}to {background-position: 90rpx;}}
</style>
注意:組件代碼不可直接復制使用,里面使用了一些我項目的封裝代碼(下面這些)。
- toast方法,是輸出提示。
- request方法,是調后端接口。
- <modal>是我封裝的一個彈窗組件,彈窗組件UI庫大把,自己套一個。參數width是百分比,我的modal組件接收的是Number,所以這里初始90,代表占屏幕90%寬度。然后再根據屏幕大小決定彈窗大小的。
- 兩個import上面有注釋說明。
最好看一遍代碼的里面的注釋,上面說的幾個地方需要自己改一下。
使用流程
功能引入此組件并定義ref,通過ref調用組件的show方法即可,傳入參數,例如:
this.$refs.$sliderCaptcha.show({// 驗證接口verifyApi: '/xxx/yyy',callback: res => {if (res) {// 驗證成功,生成后端二次驗證密文(也就是二次驗證的redisKey)(不是必須,看自己業務,如有需要的話)const captchaVerification = this.$refs.$sliderCaptcha.captchaVerificationEncrypted(res)}}
})
其他羅里吧嗦
1:滑塊滑動到缺口后,取哪里的x值?
正確缺口的開始位置,這個開始位置x值是正確的,對應后端緩存,基本大差不差。
2:怎么取?
觸摸滑塊時,touchstart方法記錄觸摸位置,滑動觸發touchmove方法,獲取當前滑動x值,然后減去起始觸摸位置,就是正確的x值。
代碼里面那個null_data.svg
我直接把源碼粘貼出來,復制到文本文件里面,然后后綴改成svg就可以用了
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="122px" height="104px" viewBox="0 0 122 104" version="1.1"><!-- Generator: Sketch 55 (78076) - https://sketchapp.com --><title>暫無相關搜索</title><desc>Created with Sketch.</desc><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="空白頁" transform="translate(-40.000000, -508.000000)"><g id="暫無相關搜索" transform="translate(40.000000, 508.000000)"><g id="分組"><g id="背景/線" fill="#C3CBD6"><g id="-"><g id="編組" transform="translate(14.200000, 9.200000)" fill-rule="nonzero"><path d="M0.8,73.1 C0.3581722,73.1 -5.68434189e-14,72.7418278 -5.68434189e-14,72.3 C-5.68434189e-14,71.8581722 0.3581722,71.5 0.8,71.5 L70.8,71.5 C71.2418278,71.5 71.6,71.8581722 71.6,72.3 C71.6,72.7418278 71.2418278,73.1 70.8,73.1 L0.8,73.1 Z M74.8,73.1 C74.3581722,73.1 74,72.7418278 74,72.3 C74,71.8581722 74.3581722,71.5 74.8,71.5 L77.3,71.5 C77.7418278,71.5 78.1,71.8581722 78.1,72.3 C78.1,72.7418278 77.7418278,73.1 77.3,73.1 L74.8,73.1 Z M83.8,73.1 C83.3581722,73.1 83,72.7418278 83,72.3 C83,71.8581722 83.3581722,71.5 83.8,71.5 L92.8,71.5 C93.2418278,71.5 93.6,71.8581722 93.6,72.3 C93.6,72.7418278 93.2418278,73.1 92.8,73.1 L83.8,73.1 Z M23.8,80.6 C23.3581722,80.6 23,80.2418278 23,79.8 C23,79.3581722 23.3581722,79 23.8,79 L30.8,79 C31.2418278,79 31.6,79.3581722 31.6,79.8 C31.6,80.2418278 31.2418278,80.6 30.8,80.6 L23.8,80.6 Z M35.3,80.6 C34.8581722,80.6 34.5,80.2418278 34.5,79.8 C34.5,79.3581722 34.8581722,79 35.3,79 L65.8,79 C66.2418278,79 66.6,79.3581722 66.6,79.8 C66.6,80.2418278 66.2418278,80.6 65.8,80.6 L35.3,80.6 Z M80,52.8 C80,51.7333333 81.6,51.7333333 81.6,52.8 L81.6,55.8 C81.6,56.2418278 81.2418278,56.6 80.8,56.6 L77.8,56.6 C76.7333333,56.6 76.7333333,55 77.8,55 L80,55 L80,52.8 Z M81.6,58.8 C81.6,59.8666667 80,59.8666667 80,58.8 L80,55.8 C80,55.3581722 80.3581722,55 80.8,55 L83.8,55 C84.8666667,55 84.8666667,56.6 83.8,56.6 L81.6,56.6 L81.6,58.8 Z M4,28.8 C4,27.7333333 5.6,27.7333333 5.6,28.8 L5.6,31.8 C5.6,32.2418278 5.2418278,32.6 4.8,32.6 L1.8,32.6 C0.733333333,32.6 0.733333333,31 1.8,31 L4,31 L4,28.8 Z M78.1,3.5 L80.8,3.5 C81.8666667,3.5 81.8666667,5.1 80.8,5.1 L78.1,5.1 L78.1,7.8 C78.1,8.86666667 76.5,8.86666667 76.5,7.8 L76.5,5.1 L73.8,5.1 C72.7333333,5.1 72.7333333,3.5 73.8,3.5 L76.5,3.5 L76.5,0.8 C76.5,-0.266666667 78.1,-0.266666667 78.1,0.8 L78.1,3.5 Z M5.6,34.8 C5.6,35.8666667 4,35.8666667 4,34.8 L4,31.8 C4,31.3581722 4.3581722,31 4.8,31 L7.8,31 C8.86666667,31 8.86666667,32.6 7.8,32.6 L5.6,32.6 L5.6,34.8 Z" id="Path-2"></path><path d="M14.0928932,61.1431458 C14.5642977,60.6717412 15.2714045,61.378848 14.8,61.8502525 L13.7393398,62.9109127 C13.5440777,63.1061748 13.2274952,63.1061748 13.032233,62.9109127 L11.9715729,61.8502525 C11.5001684,61.378848 12.2072751,60.6717412 12.6786797,61.1431458 L13.3857864,61.8502525 L14.0928932,61.1431458 Z M12.6786797,63.9715729 C12.2072751,64.4429774 11.5001684,63.7358706 11.9715729,63.2644661 L13.032233,62.2038059 C13.2274952,62.0085438 13.5440777,62.0085438 13.7393398,62.2038059 L14.8,63.2644661 C15.2714045,63.7358706 14.5642977,64.4429774 14.0928932,63.9715729 L13.3857864,63.2644661 L12.6786797,63.9715729 Z M22.9213203,8.8 C23.3927249,8.32859548 24.0998316,9.03570226 23.6284271,9.50710678 L22.567767,10.567767 C22.3725048,10.7630291 22.0559223,10.7630291 21.8606602,10.567767 L20.8,9.50710678 C20.3285955,9.03570226 21.0357023,8.32859548 21.5071068,8.8 L22.2142136,9.50710678 L22.9213203,8.8 Z M21.5071068,11.6284271 C21.0357023,12.0998316 20.3285955,11.3927249 20.8,10.9213203 L21.8606602,9.86066017 C22.0559223,9.66539803 22.3725048,9.66539803 22.567767,9.86066017 L23.6284271,10.9213203 C24.0998316,11.3927249 23.3927249,12.0998316 22.9213203,11.6284271 L22.2142136,10.9213203 L21.5071068,11.6284271 Z" id="Path復制"></path></g></g></g><g id="Group-6" transform="translate(37.000000, 29.000000)"><g id="分組" stroke="#C3CBD6"><path d="M3,-1.0658141e-14 L35,-1.0658141e-14 C36.6568542,-1.09625002e-14 38,1.34314575 38,3 L38,42 C38,43.6568542 36.6568542,45 35,45 L3,45 C1.34314575,45 2.02906125e-16,43.6568542 0,42 L0,3 C-2.02906125e-16,1.34314575 1.34314575,-1.03537818e-14 3,-1.0658141e-14 Z" id="矩形" stroke-width="1.6" fill="#FFFFFF"></path><path d="M7.8,26.8 L12.8,26.8 L7.8,26.8 Z M7.8,19.8 L11.8,19.8 L7.8,19.8 Z M7.8,12.8 L13.8,12.8 L7.8,12.8 Z" id="Stroke-16" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></g><g id="分組-2" transform="translate(15.000000, 4.000000)"><path d="M26.8450231,25.0101574 L32.8450231,25.0101574 L32.8370236,35.9124666 C32.8358076,37.5696764 31.4920325,38.9124658 29.8348223,38.9124658 L29.8348223,38.9124658 C28.1791833,38.9124658 26.8370228,37.5703052 26.8370228,35.9146662 C26.8370228,35.913933 26.837023,35.9131998 26.8370236,35.9124666 L26.8450231,25.0101574 Z" id="矩形" stroke="#C3CBD6" stroke-width="1.6" fill="#F5F7F9" transform="translate(29.839923, 31.961312) rotate(-42.000000) translate(-29.839923, -31.961312) "></path><circle id="橢圓形" stroke="#C3CBD6" stroke-width="1.6" fill="#F5F7F9" cx="16.5" cy="16.5" r="16.5"></circle><circle id="橢圓形-copy" stroke="#C3CBD6" stroke-width="1.6" fill="#FFFFFF" cx="16.5" cy="16.5" r="12.5"></circle><path d="M17.6131592,19.685 C17.3091168,19.7925464 15.7049189,19.8010303 15.4349189,19.685 C15.1649189,19.5689697 15.1349189,19.43 15.1349189,18.995 C15.1349189,17.96 15.3149189,17.36 15.7949189,16.775 C16.1549189,16.355 16.2749189,16.265 17.3399189,15.62 C18.0749189,15.185 18.3149189,14.87 18.3149189,14.375 C18.3149189,13.67 17.7899189,13.25 16.8899189,13.25 C16.2149189,13.25 15.7199189,13.49 15.4349189,13.955 C15.2849189,14.195 15.1949189,14.2841284 15.1649189,14.87 C15.1349189,15.4558716 11.9819336,15.62 12.0749189,14.585 C12.1679042,13.55 12.4649189,12.92 13.0349189,12.29 C13.9049189,11.3 15.2399189,10.79 16.9649189,10.79 C19.7249189,10.79 21.4499189,12.095 21.4499189,14.165 C21.4499189,15.08 21.1499189,15.785 20.5199189,16.37 C20.1599189,16.7 20.0099189,16.805 18.7049189,17.615 C18.0749189,18.02 17.8499189,18.41 17.8499189,19.1 C17.8499189,19.28 17.9172016,19.5774536 17.6131592,19.685 Z M16.4174189,20.705 L16.5974189,20.705 C17.3802825,20.705 18.0149189,21.3396364 18.0149189,22.1225 L18.0149189,22.1225 C18.0149189,22.9053636 17.3802825,23.54 16.5974189,23.54 L16.4174189,23.54 C15.6345553,23.54 14.9999189,22.9053636 14.9999189,22.1225 L14.9999189,22.1225 C14.9999189,21.3396364 15.6345553,20.705 16.4174189,20.705 Z" id="?" fill="#C3CBD6" fill-rule="nonzero"></path></g></g></g></g></g></g>
</svg>
碼字不易,于你有利,勿忘點贊