由于步骤太多,字数太多,废话也太多,所以前后端分开讲了,后端文章请看:
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 {
// 滑块宽高度 px
slideBlockWidth: 47,
// 背景图宽度 px
slideImageWidth: 310,
// 背景图高度 px
slideImageHeight: 155,
// 弹窗占据屏幕 90%
modalWidth: 90,
// 弹窗内部左右边距20px
modalLRpadding: 20,
// 缩放比率,不同屏幕的手机,必须适配
scaleRatio: 0,
// 滑动起始X位置 px
startX: 0,
// 滑动停止位置,也就是距离起始位置长度 px
lastLeft: 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]
},
// 背景图base64
sliderCaptchaBackBase64 () {
const { originalImageBase64 } = this.captchaOption
if (!originalImageBase64) {
return ''
}
return 'data:image/png;base64,' + originalImageBase64
},
// 滑块base64
sliderCaptchaBlockBase64 () {
const { jigsawImageBase64 } = this.captchaOption
if (!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 = option
this.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 * 2
const modalInternalWidth = modalWidthPx - modalLRpaddingSum
const onePercentagePx = windowWidth * 0.01
if (modalInternalWidth > this.slideImageWidth) {
// 如果屏幕宽度大于初始化滑块背景图宽度,则不缩放
this.modalWidth = util.toFixed((this.slideImageWidth + modalLRpaddingSum) / onePercentagePx, 2)
} else if (modalInternalWidth < this.slideImageWidth) {
// 否则缩放滑块背景图和滑块
this.scaleRatio = modalInternalWidth / this.slideImageWidth
this.slideImageWidth = modalInternalWidth
this.slideImageHeight = this.slideImageHeight * this.scaleRatio
this.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 = repCode
this.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.clientX
const width = this.slideImageWidth - this.slideBlockWidth - 1
let left = pageX - this.startX
left = left < 0 ? 0 : (left >= width ? width : left)
this.lastLeft = left
},
// 滑动结束(松手),验证
touchend () {
/**
* callback 父页面验证结果回调
* verifyCaptchaCallback 整个验证由父页面处理
* verifyApi 验证后端接口
*
* callback 和 verifyCaptchaCallback 二选一
* 选callback,verifyApi必传,让这个这个组件调用验证接口
*/
const { callback, verifyCaptchaCallback, verifyApi } = this.parentOption
if (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.secretKey
this.close()
} else {
// 验证失败重新获取
this.resetSlideCaptcha(true)
}
callback(res)
})
}
} else {
this.resetSlideCaptcha(true)
}
},
// 重置
resetSlideCaptcha (generateFlag) {
this.lastLeft = 0
this.startX = 0
generateFlag && 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 } = res
if (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 - 1
return 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 + '---' + originalPointJson
const 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>
码字不易,于你有利,勿忘点赞