在Vue3项目中实现文字点选验证的人机校验

        在现代Web应用中,人机校验(CAPTCHA)是防止恶意机器人自动访问的重要措施之一。除了常见的图片验证码、滑动验证外,文字点选验证也是一种有效且用户友好的方式。本文将介绍如何在Vue项目中实现登录时的文字点选验证功能。

我们的设计目标包括:

  • 实现一个用户友好的登录验证码。
  • 提高验证码的安全性,防止自动化攻击。
  • 保证验证码字符及其位置的随机性。
  • 提供良好的用户体验。

这里没用进行样式优化,不是很好看,就是为了实现功能。相关样式可自行修改,下面是样式图

 这里是选中正确的样式,可以自行更改,在代码中会进行标识

当然为了复用性和方便就把验证做成了组件,在组建中新建文件Perustarkistus.vue

根据图效果分析也就两块区域,1.显示文字顺序。2.显示图片文字

显示文字顺序没什么可布局的,一个块级元素就可以

   <p>请按照 ‘XXXX’ 的顺序点击</p>

显示图片和文字就需要进行定位设计了


<div id='perustarkistus' class='perustarkistus'>
  <img src="" alt="">
  <div class="positionDiv">
    <span>文字展示</span>
    <span>点击的顺序</span>
  </div>
</div>
<!-- 上面没写template -->
<style lang="less" scoped>
#perustarkistus.perustarkistus {
  position: relative;

  div.positionDiv {
    position: absolute;
    cursor: pointer;
    text-align: center;
    font-weight: bold;
    z-index: 1;

    span {
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      z-index: 1;
      background-color: rgba(255, 255, 255, .7);
    }
  }
}
</style>

        接下来是要考虑需要的字段和字段关系,在这里所有用到的数字和设计都用了动态数据,可自行修改,方便自行设计

文字点选验证逻辑

        文字点选验证要求用户从一组随机字符中,点击正确的字符。核心逻辑包含以下几部分:

  1. 生成随机字符集合:从一个预定义的字符集(例如英文字母和数字)中随机选择一些字符。
  2. 标记正确字符:在随机字符中挑选出几个正确字符。
  3. 用户点击验证:用户点击字符后,记录用户选择并判断是否满足验证条件。

随机生成文字的位置

        为了增加验证码的复杂度和安全性,我们不仅要随机生成字符,还要随机生成它们在页面上的位置。这可以通过以下步骤实现:

  1. 生成字符:随机选择要展示的字符。
  2. 确定位置:为每个字符分配一个随机位置,确保字符不会重叠或超出边界。
  3. 渲染字符:按照随机位置在页面上渲染字符。
// 用来随机选择文字
const textString = '凭趟押王玉抹凶超护报抢趁凳抬出击披越趋跳叉又边友及反双达悟辽发辣悄叔取受变辪辫叙磁槽蛛蛙蛇韵音蛋槐蛾磨蛮砌砍码霸露研砖婶云御泰京亭亮泉亩泊享交泄亦产得徐亡菠徒亿穗武委姐姑箱钓姓钞姜薯钟薪管箭箩钉始针薄沉莫忙仪们代沃令以志沙任忍份忌沟仿莲仰心刑灰灯灭刊火分切慕刃刀蹲炒躁炕勺勾勿勤炉炊炎勒龙炸点身躬炼龟躲勇龄勉躺炮炭励劲劳商眠易眯星眨啊昆昂昌眼昏明啦省显昼看眉春'
const bgPictures = [
  'https://img2.baidu.com/it/u=1381481047,1529970259&fm=253&fmt=auto&app=138&f=JPEG?w=752&h=500',
  'https://img2.baidu.com/it/u=4286724097,1475456570&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500',
  'https://img1.baidu.com/it/u=2310170655,486191485&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281',
  'https://img2.baidu.com/it/u=2526401426,2132302010&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281',
  'https://img2.baidu.com/it/u=2279721922,3725358742&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
  'https://img1.baidu.com/it/u=2605372625,2257936617&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=313',
  'https://img2.baidu.com/it/u=188805366,3528195373&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=675',
  'https://img0.baidu.com/it/u=381886100,3541087750&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281',
  'https://img2.baidu.com/it/u=769068768,1914010451&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
]   // 背景图片列表

// 这里的数据可以加入 textPosition 里面 因为是死数据,没啥影响
const allTextNum = 4 // 生成文字的个数/需要点击的个数
const textWidth = 30 // 文字的宽度
const parentWidth = 300  // 宽度
const parentHeight = 180  // 高度
const angleRange = 90  // 旋转角度范围 例如值为90 就是-90~90
const textRank = ref('')  // 文本顺序
const imageSrc = ref('') // 随机选择的图片src
const clickTextRank = ref('')// 点击文本的顺序
const textPosition = reactive({
  textPositionData: [] as textPositionType[]
}) // 文本生成的位置

接下来要考虑 textPosition 里面 textPositionData 的类型 textPositionType了

type textPositionType = {
  x: number
  y: number
  width: number
  height: number
  transform: number
  text: string
}

首先考虑就是定位的距离,x->left 左定位距离  y->top 上定位距离主要就是这两个

width和height就是文字的宽度和高度,没啥用可以去掉

transform用来设置文字旋转角度的,text就是文字了

    字段已经准备好了,现在就要考虑生成textPositionData的数据了,由于考虑了文字的个数是个变数,所以就直接写了一个函数,用传入需要生成文字的个数进行生成。
// 检测两个矩形是否重叠的函数
const isOverlapping = (rect1: textPositionType, rect2: textPositionType) => {
  return !(
      rect1.x + rect1.width <= rect2.x ||
      rect2.x + rect2.width <= rect1.x ||
      rect1.y + rect1.height <= rect2.y ||
      rect2.y + rect2.height <= rect1.y
  );
}
// 生成随机位置的函数 
const generateRandomPosition = (existingPositions: textPositionType[], parentWidth: number, parentHeight: number, elementWidth: number, elementHeight: number) => {
  let position;
  let overlap;

  do {
    overlap = false;
    position = {
      x: Math.floor(Math.random() * (parentWidth - elementWidth)),
      y: Math.floor(Math.random() * (parentHeight - elementHeight)),
      width: elementWidth,
      height: elementHeight,
      transform: Math.floor(Math.random() * (angleRange + angleRange + 1)) - angleRange,
      text: ''
    };

    // 检查新位置是否与现有位置重叠
    for (const existing of existingPositions) {
      if (isOverlapping(position, existing)) {
        overlap = true;
        break;
      }
    }
  } while (overlap);

  return position;
}
const changeSelectImage = () => {
  const newPositions: textPositionType[] = [];  // 用来生成文字定位信息
  // (所需字符串的长度)    allTextNum:生成点击文字的个数
  let textOptionArr = getRandomUniqueIndexes(textString.length, allTextNum)
  let textSelectString = ''  // 生成的文本顺序
  for (let i = 0; i < allTextNum; i++) {
        // 位置信息     图片宽度    图片高度    文本宽度
    const newPosition = generateRandomPosition(newPositions, parentWidth, parentHeight, textWidth, textWidth);
    newPositions.push({...newPosition, text: textString[textOptionArr[i]]});
    textSelectString += textString[textOptionArr[i]]
  }
  textRank.value = textSelectString
  textPosition.textPositionData = newPositions
}






// 下面是一个函数进行随机数生成  第一个参数所需字符串长度   第二个参数需要生成的数量 这里只是复制进来的 改的话直接删除export就行了
export const getRandomUniqueIndexes = (max: number, count: number) => {
    const indexes: number[] = [];
    while (indexes.length < count) {
        const randomIndex = Math.floor(Math.random() * max);
        if (!indexes.includes(randomIndex)) {
            indexes.push(randomIndex);
        }
    }
    return indexes;
};

所有的准备都已经做完了,剩下的就是调用函数赋值就行了。这里直接放全文了,文中有没整理的多余的字段,可自行删除。

<template>
  <div>
    <p>请按照 ‘{{ textRank }}’ 的顺序点击</p>
    <div id='perustarkistus' class='perustarkistus'>
      <img :src="imageSrc" :style="{width:parentWidth + 'px', height: parentHeight + 'px'}" alt="">
      <div v-for="(item,indexS) in getTextPositionData"
           :key="indexS" :style="{
                    left: item.x + 'px',
                    top: item.y + 'px',
                    width: textWidth + 'px',
                    height: textWidth + 'px',
                    transform: `rotate(${item.transform}deg)`,
                    fontSize: 20 + 'px',
                    lineHeight: `${textWidth}px`
                }"
           class="positionDiv"
           @click="textClickFun(item.text)">
        <span>{{ item.text }}</span>
        <span v-if="item.rank !== null">{{ item.rank + 1 }}</span>
      </div>
    </div>
  </div>
</template>


<script lang="ts">
import {computed, defineComponent, onBeforeMount, reactive, ref, watch} from 'vue'
import {getRandomUniqueIndexes} from "@/assets/js/someFunction";
import {ElMessage} from "element-plus";

export default defineComponent({
  name: "Perustarkistus",
  setup() {
    type textPositionType = {
      x: number
      y: number
      width: number
      height: number
      transform: number
      text: string
    }
    // 用来随机选择文字
    const textString = '凭趟押王玉抹凶超护报抢趁凳抬出击披越趋跳叉又边友及反双达悟辽发辣悄叔取受变辪辫叙磁槽蛛蛙蛇韵音蛋槐蛾磨蛮砌砍码霸露研砖婶云御泰京亭亮泉亩泊享交泄亦产得徐亡菠徒亿穗武委姐姑箱钓姓钞姜薯钟薪管箭箩钉始针薄沉莫忙仪们代沃令以志沙任忍份忌沟仿莲仰心刑灰灯灭刊火分切慕刃刀蹲炒躁炕勺勾勿勤炉炊炎勒龙炸点身躬炼龟躲勇龄勉躺炮炭励劲劳商眠易眯星眨啊昆昂昌眼昏明啦省显昼看眉春'
    const bgPictures = [
      'https://img2.baidu.com/it/u=1381481047,1529970259&fm=253&fmt=auto&app=138&f=JPEG?w=752&h=500',
      'https://img2.baidu.com/it/u=4286724097,1475456570&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500',
      'https://img1.baidu.com/it/u=2310170655,486191485&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281',
      'https://img2.baidu.com/it/u=2526401426,2132302010&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281',
      'https://img2.baidu.com/it/u=2279721922,3725358742&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
      'https://img1.baidu.com/it/u=2605372625,2257936617&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=313',
      'https://img2.baidu.com/it/u=188805366,3528195373&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=675',
      'https://img0.baidu.com/it/u=381886100,3541087750&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281',
      'https://img2.baidu.com/it/u=769068768,1914010451&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
    ]   // 背景图片列表

    // 这里的数据可以加入 textPosition 里面 因为是死数据,没啥影响
    // const textNum = 4  // 需要点击的个数
    const allTextNum = 4 // 生成文字的个数/需要点击的个数
    const textWidth = 30 // 文字的宽度
    const parentWidth = 300  // 宽度
    const parentHeight = 180  // 高度
    const angleRange = 90  // 旋转角度范围 例如值为90 就是-90~90
    const textRank = ref('')  // 文本顺序
    const clickTextRank = ref('')// 点击文本的顺序
    const textPosition = reactive({
      textPositionData: [] as textPositionType[]
    }) // 文本生成的位置
    const imageSrc = ref('') // 随机选择的图片src
    // 文字点击事件
    const textClickFun = (text: string) => {
      if (clickTextRank.value.includes(text)) {
        clickTextRank.value = clickTextRank.value.replace(text, '')
      } else {
        clickTextRank.value += text
      }
      console.log('clickTextRank', clickTextRank.value)
    }

    // 检测两个矩形是否重叠的函数
    const isOverlapping = (rect1: textPositionType, rect2: textPositionType) => {
      return !(
          rect1.x + rect1.width <= rect2.x ||
          rect2.x + rect2.width <= rect1.x ||
          rect1.y + rect1.height <= rect2.y ||
          rect2.y + rect2.height <= rect1.y
      );
    }
    // 生成随机位置的函数
    const generateRandomPosition = (existingPositions: textPositionType[], parentWidth: number, parentHeight: number, elementWidth: number, elementHeight: number) => {
      let position;
      let overlap;

      do {
        overlap = false;
        position = {
          x: Math.floor(Math.random() * (parentWidth - elementWidth)),
          y: Math.floor(Math.random() * (parentHeight - elementHeight)),
          width: elementWidth,
          height: elementHeight,
          transform: Math.floor(Math.random() * (angleRange + angleRange + 1)) - angleRange,
          text: ''
        };

        // 检查新位置是否与现有位置重叠
        for (const existing of existingPositions) {
          if (isOverlapping(position, existing)) {
            overlap = true;
            break;
          }
        }
      } while (overlap);

      return position;
    }
    const changeSelectImage = () => {
      const newPositions: textPositionType[] = [];  // 用来生成文字定位信息
      // (所需字符串的长度)    allTextNum:生成点击文字的个数
      let textOptionArr = getRandomUniqueIndexes(textString.length, allTextNum)
      let textSelectString = ''  // 文本顺序
      for (let i = 0; i < allTextNum; i++) {
        // 位置信息     图片宽度    图片高度    文本宽度
        const newPosition = generateRandomPosition(newPositions, parentWidth, parentHeight, textWidth, textWidth);
        newPositions.push({...newPosition, text: textString[textOptionArr[i]]});
        textSelectString += textString[textOptionArr[i]]
      }
      textRank.value = textSelectString
      console.log('newPositions', newPositions)
      textPosition.textPositionData = newPositions
    }

    const getTextPositionData = computed(() => {
      const data: any = []
      textPosition.textPositionData.forEach(item => {
        const rankIndex = clickTextRank.value.indexOf(item.text);
        data.push({
          ...item,
          rank: rankIndex !== -1 ? rankIndex : null
        });
      });

      console.log('data', data)
      return data
    })
    onBeforeMount(() => {
      imageSrc.value = bgPictures[Math.floor(Math.random() * 9)]
      changeSelectImage()
    })

    watch(clickTextRank, (newValue, oldValue) => {
      if (clickTextRank.value.length === allTextNum) {
        if (clickTextRank.value === textRank.value) {
          ElMessage({
            message: '正确',
            type: 'success',
          })
          // 某些操作···
        } else {
          ElMessage.error('错误')
          clickTextRank.value = ''
          changeSelectImage()
        }
      }
    })
    return {
      imageSrc,
      parentWidth,
      parentHeight,
      textPosition,
      textWidth,
      allTextNum,
      clickTextRank,
      textClickFun,
      textRank,
      getTextPositionData
    }

  },

})
</script>


<style lang="less" scoped>
#perustarkistus.perustarkistus {
  position: relative;

  div.positionDiv {
    position: absolute;
    cursor: pointer;
    text-align: center;
    font-weight: bold;
    z-index: 1;

    span {
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      z-index: 1;
      background-color: rgba(255, 255, 255, .7);
    }
  }
}
</style>

  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫和老许

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值